Build a macOS Barcode Scanner with SwiftUI and a C++ Barcode SDK
Developing a barcode scanner app for macOS comes with unique challenges, particularly due to the limited availability of barcode SDKs with a native Swift API for the platform. The Dynamsoft Capture Vision SDK covers that gap with a C++ core that ships macOS dylibs for barcode, MRZ, and document scanning. In this tutorial, we call the C++ shared libraries from an Objective-C++ wrapper and drive them from SwiftUI, moving step by step from C++ to Objective-C and finally to Swift.
What you’ll build: A native macOS barcode scanner app using SwiftUI that captures live camera frames and decodes 1D/2D barcodes in real time via the Dynamsoft Capture Vision C++ SDK, bridged through Objective-C++.
Key Takeaways
- The Dynamsoft Capture Vision C++ SDK can be integrated into a SwiftUI macOS app by wrapping C++ calls in an Objective-C++ (
.mm) file and exposing them through a bridging header. - Camera frames are captured with
AVCaptureVideoDataOutputand passed to the C++ barcode engine as raw pixel buffers — no intermediate image conversion is needed. - macOS camera preview coordinates require a vertical flip before barcode bounding-box overlays align correctly with the live preview.
- This approach extends to iOS with minimal changes because SwiftUI and the C++ SDK are cross-platform.
Common Developer Questions
How do I integrate a C++ barcode SDK into a SwiftUI macOS app?
Place the C++ SDK headers and dylibs in the Xcode project, add an Objective-C++ wrapper, and expose Swift-friendly methods through a bridging header. SwiftUI can then call the wrapper class without interacting with raw C++ types.
How do I capture camera frames in macOS SwiftUI and pass them to a C++ library for barcode detection?
Use AVCaptureVideoDataOutput to receive pixel buffers from the camera, extract the frame bytes in the Objective-C++ bridge, and pass those bytes directly to the C++ capture API. That avoids saving intermediate images and keeps the scan loop real time.
Why are barcode bounding boxes mirrored or flipped on the macOS camera preview?
The preview and the decoded result coordinates do not always share the same vertical origin, so the overlay often needs a Y-axis flip before drawing. Without that transform, the boxes appear mirrored or vertically offset from the actual barcode location.
This article is Part 3 in a 6-Part Series.
- Part 1 - How to Build an iOS QR Code and Barcode Scanner with SwiftUI on Apple Silicon
- Part 2 - Build an iOS Passport and ID MRZ Scanner with SwiftUI and Dynamsoft Capture Vision
- Part 3 - Build a macOS Barcode Scanner with SwiftUI and a C++ Barcode SDK
- Part 4 - Build a SwiftUI Barcode Scanner for iOS and macOS with Dynamsoft Capture Vision
- Part 5 - Build a Cross-Platform SwiftUI Document Scanner for macOS and iOS
- Part 6 - How to Build a macOS Framework Wrapping C++ in Objective-C++ for Swift Barcode Scanning
macOS 1D/2D Barcode Scanner Demo Video
Prerequisites
- Get a 30-day free trial license for the Dynamsoft Capture Vision SDK.
- Obtain the C++ SDK headers and macOS dylibs. The Dynamsoft Capture Vision bundle on pypi packages them for Windows, Linux, and macOS; extract the
*.hand*.dylibfiles. The sample repository already vendors version 3.6.50.9329 inexamples/dcv, so you can skip this download if you start from the sample. - Xcode 16+ installed on macOS. The sample vendors the Dynamsoft Capture Vision C++ libraries 3.6.50.9329 (universal arm64 + x86_64 dylibs) in
examples/dcv.
Step 1: Set Up the Xcode Project for macOS Barcode Scanning
Since the current target of the project is macOS, with potential future extension to iOS, we’ll select the Multiplatform template when creating the project in Xcode.

Next, copy the.h and .dylib files to a new folder named dbr and drag the folder into the project.

Navigate to Build Phases -> Link Binary With Libraries and add the *.dylib files. Ensure that the supported platform is set to macOS only.

Click the + button and select New Copy Files Phase. Set the destination to Frameworks and add the *.dylib files. This step bundles the *.dylib files with the app.

Finally, open the .entitlements file and add the camera permission for the macOS app.

Step 2: Bridge the C++ Barcode Detection API to Objective-C
To invoke C++ functions in Swift, we need to create an Objective-C++ wrapper.
-
Create an Objective-C bridging header file named
DCV-Bridging-Header.h. The C++ includes and namespaces sit behind__cplusplusandTARGET_OS_IPHONEguards so the same header can be imported from Objective-C, Objective-C++, and Swift:#ifndef DCV_Bridging_Header_h #define DCV_Bridging_Header_h #include <TargetConditionals.h> #if defined(__APPLE__) && defined(__MACH__) && !TARGET_OS_IPHONE #ifdef __cplusplus #include "DynamsoftCaptureVisionRouter.h" #include "DynamsoftUtility.h" using namespace dynamsoft::license; using namespace dynamsoft::cvr; using namespace dynamsoft::dbr; using namespace dynamsoft::utility; using namespace dynamsoft::basic_structures; #endif /* __cplusplus */ #import <Foundation/Foundation.h> @interface CaptureVisionWrapper : NSObject + (int)initializeLicense:(NSString *)licenseKey; - (NSArray *)captureImageWithData:(void *)baseAddress width:(int)width height:(int)height stride:(int)stride pixelFormat:(OSType)pixelFormat; @end #endif #endif /* DCV_Bridging_Header_h */Explanation
- The code inside
#ifdef __cplusplusincludesDynamsoftCaptureVisionRouter.handDynamsoftUtility.hand pulls in the Dynamsoft namespaces. C++ syntax is not valid in plain Objective-C, so the guard keeps the header importable from both languages. captureImageWithData:takes the rawvoid *base address of the pixel buffer. Reusing the camera buffer avoids copying a full frame on every detection pass.TARGET_OS_IPHONEkeeps the wrapper compiled for macOS only, so the iOS target of a multiplatform project can use the Swift package instead.
- The code inside
-
In
Build Settings, set theObjective-C Bridging Headerto the created header file.
-
Create a
dcv.mmfile, which allows calling C++ functions, using C++ libraries, and interacting with C++ objects within Objective-C methods.-
Import the bridging header file:
#import <CoreVideo/CoreVideo.h> #import <Foundation/Foundation.h> #import "DCV-Bridging-Header.h" #if defined(__APPLE__) && defined(__MACH__) && !TARGET_OS_IPHONE @implementation CaptureVisionWrapper { CCaptureVisionRouter *cvr; } -
Initialize the license key:
+ (int)initializeLicense:(NSString *)licenseKey { char errorMsgBuffer[512]; const char *licenseCString = [licenseKey UTF8String]; int ret = CLicenseManager::InitLicense(licenseCString, errorMsgBuffer, sizeof(errorMsgBuffer)); if (ret != 0) { NSString *errorMessage = [NSString stringWithUTF8String:errorMsgBuffer]; NSLog(@"License initialization failed: %@", errorMessage); } else { NSLog(@"License initialized successfully"); } return ret; } -
Manage the lifecycle of the
CCaptureVisionRouterobject. No JSON template is loaded here because detection uses the built-inPT_READ_BARCODESpreset:- (instancetype)init { self = [super init]; if (self) { cvr = new CCaptureVisionRouter(); } return self; } - (void)dealloc { if (cvr) { delete cvr; cvr = nullptr; } } -
Convert the frame image format from
OSTypetoImagePixelFormat. The capture session is configured for32BGRA:- (ImagePixelFormat)mapPixelFormat:(OSType)pixelFormat { switch (pixelFormat) { case kCVPixelFormatType_32BGRA: return IPF_ABGR_8888; default: return IPF_NV21; } } -
Decode barcodes from an image and return an array of barcode results:
- (NSArray *)captureImageWithData:(void *)baseAddress width:(int)width height:(int)height stride:(int)stride pixelFormat:(OSType)pixelFormat { ImagePixelFormat sdkPixelFormat = [self mapPixelFormat:pixelFormat]; CImageData *imageStruct = new CImageData(stride * height, (unsigned char *)baseAddress, width, height, stride, sdkPixelFormat); CCapturedResult *result = cvr->Capture(imageStruct, CPresetTemplate::PT_READ_BARCODES); if (result->GetErrorCode() != 0) { NSLog(@"Error code: %d", result->GetErrorCode()); } CDecodedBarcodesResult *barcodeResult = result->GetDecodedBarcodesResult(); if (barcodeResult == nullptr || barcodeResult->GetItemsCount() == 0) { NSLog(@"No barcode found"); delete imageStruct; return nil; } int barcodeResultItemCount = barcodeResult->GetItemsCount(); NSLog(@"Total barcode(s) found: %d", barcodeResultItemCount); NSMutableArray *barcodeArray = [NSMutableArray array]; for (int j = 0; j < barcodeResultItemCount; j++) { const CBarcodeResultItem *barcodeResultItem = barcodeResult->GetItem(j); const char *format = barcodeResultItem->GetFormatString(); const char *text = barcodeResultItem->GetText(); CQuadrilateral quad = barcodeResultItem->GetLocation(); // Keep alive before use CPoint *points = quad.points; NSDictionary *barcodeData = @{ @"format" : [NSString stringWithUTF8String:format], @"text" : [NSString stringWithUTF8String:text], @"points" : @[ @{@"x" : @(points[0][0]), @"y" : @(height - points[0][1])}, @{@"x" : @(points[1][0]), @"y" : @(height - points[1][1])}, @{@"x" : @(points[2][0]), @"y" : @(height - points[2][1])}, @{@"x" : @(points[3][0]), @"y" : @(height - points[3][1])} ] }; [barcodeArray addObject:barcodeData]; } if (barcodeResult) barcodeResult->Release(); result->Release(); delete imageStruct; return [barcodeArray copy]; } @end #endif
Explanation
GetLocation()returns aCQuadrilateralby value, so the result is stored in a local variable beforepointsis read. CallingbarcodeResultItem->GetLocation().pointsdirectly would leave a pointer into a destroyed temporary.- The Y coordinate is flipped with
height - points[i][1]because the C++ coordinate origin is the top-left of the frame while the AppKit overlay draws from the bottom-left. - Everything the C++ API allocates — the
CImageData, the decoded result, and the captured result — is released before returning.
-
Step 3: Create a Camera View Controller in Swift
Create a CameraViewController.swift file and add the following code to set up a camera view:
import AVFoundation
import Accelerate
import SwiftUI
import Cocoa
typealias ViewController = NSViewController
typealias ImageType = NSImage
class CameraViewController: ViewController,
AVCapturePhotoCaptureDelegate, AVCaptureVideoDataOutputSampleBufferDelegate
{
var captureSession: AVCaptureSession!
var photoOutput: AVCapturePhotoOutput!
var previewLayer: AVCaptureVideoPreviewLayer!
var overlayView: BarcodeOverlayView!
let cv = CaptureVisionWrapper()
override func viewDidLoad() {
super.viewDidLoad()
let licenseKey = "LICENSE-KEY"
let result = CaptureVisionWrapper.initializeLicense(licenseKey)
if result != 0 {
print("Failed to initialize license with error code: \(result)")
}
setupCamera()
}
func setupCamera() {
captureSession = AVCaptureSession()
captureSession.sessionPreset = .photo
guard let camera = AVCaptureDevice.default(for: .video) else {
print("Unable to access the camera!")
return
}
do {
let input = try AVCaptureDeviceInput(device: camera)
photoOutput = AVCapturePhotoOutput()
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.videoSettings = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
]
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
if captureSession.canAddInput(input) && captureSession.canAddOutput(photoOutput) {
captureSession.addInput(input)
captureSession.addOutput(photoOutput)
captureSession.addOutput(videoOutput)
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.frame = view.bounds
view.layer = CALayer()
view.wantsLayer = true
view.layer?.insertSublayer(previewLayer, at: 0)
DispatchQueue.global(qos: .userInitiated).async {
self.captureSession.startRunning()
}
}
} catch {
print("Error Unable to initialize camera: \(error.localizedDescription)")
}
}
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
processCameraFrame(pixelBuffer)
}
override func viewDidLayout() {
super.viewDidLayout()
previewLayer?.frame = view.bounds
overlayView?.frame = view.bounds
}
}
Explanation
- The
CameraViewControllerclass subclassesNSViewControllerand conforms toAVCaptureVideoDataOutputSampleBufferDelegate, so it receives every camera frame. photoOutputis created before thecanAddOutputcheck. The session is configured for32BGRAframes because the wrapper maps that format toIPF_ABGR_8888.overlayViewis declared here and attached in Step 6, above the preview layer, so the barcode boxes are drawn on top of the video.
Step 4: Create a SwiftUI Camera View
Create a CameraView.swift file to integrate the CameraViewController into a SwiftUI view hierarchy:
import AVFoundation
import SwiftUI
struct CameraView: NSViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSViewController(context: Context) -> CameraViewController {
let cameraViewController = CameraViewController()
context.coordinator.cameraViewController = cameraViewController
return cameraViewController
}
func updateNSViewController(_ nsViewController: CameraViewController, context: Context) {
}
class Coordinator: NSObject {
var parent: CameraView
var cameraViewController: CameraViewController?
init(_ parent: CameraView) {
self.parent = parent
}
}
}
Explanation
makeCoordinatorcreates aCoordinatorinstance to facilitate communication betweenCameraViewandCameraViewController.makeNSViewControllerinitializes theCameraViewControllerto be embedded in the SwiftUI view.updateNSViewControllerallows dynamic updates toCameraViewControllerwhen theCameraViewstate changes.
Step 5: Decode Barcodes from Camera Frames in Swift
Swift and Objective-C interoperate directly, so processCameraFrame can call the Objective-C++ wrapper with the pixel buffer address and let C++ do the decoding.
func processCameraFrame(_ pixelBuffer: CVPixelBuffer) {
let previewWidth = CVPixelBufferGetWidth(pixelBuffer)
let previewHeight = CVPixelBufferGetHeight(pixelBuffer)
DispatchQueue.main.async { [weak self] in
self?.overlayView.cameraPreviewSize = CGSize(width: previewWidth, height: previewHeight)
}
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else { return }
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
let barcodeArray = cv.captureImage(withData: baseAddress,
width: Int32(width),
height: Int32(height),
stride: Int32(bytesPerRow),
pixelFormat: pixelFormat) as? [[String: Any]] ?? []
DispatchQueue.main.async { [weak self] in
self?.overlayView.barcodeData = barcodeArray
self?.overlayView.setNeedsDisplay(self?.overlayView.bounds ?? .zero)
}
}
The selector captureImageWithData:width:height:stride:pixelFormat: is imported into Swift as captureImage(withData:width:height:stride:pixelFormat:), which is why the call spells out withData: instead of with:.
Step 6: Draw Barcode Results with an Overlay View
To display barcode detection results in real-time, overlay the bounding boxes on the camera preview.
-
Create a
BarcodeOverlayView.swiftfile and add the following code:import SwiftUI import Cocoa typealias OverlayView = NSView typealias OverlayColor = NSColor class BarcodeOverlayView: OverlayView { var barcodeData: [[String: Any]] = [] var cameraPreviewSize: CGSize = .zero override func draw(_ rect: CGRect) { guard cameraPreviewSize != .zero else { return } guard let context = NSGraphicsContext.current?.cgContext else { return } let overlaySize = bounds.size for barcode in barcodeData { guard let points = barcode["points"] as? [[String: NSNumber]], let format = barcode["format"] as? String, let text = barcode["text"] as? String else { continue } let convertedPoints = points.map { point -> CGPoint in let x = CGFloat(point["x"]!.doubleValue) let y = CGFloat(point["y"]!.doubleValue) return convertToOverlayCoordinates( cameraPoint: CGPoint(x: x, y: y), overlaySize: overlaySize) } context.setStrokeColor(OverlayColor.red.cgColor) context.setLineWidth(2.0) if let firstPoint = convertedPoints.first { context.beginPath() context.move(to: firstPoint) for point in convertedPoints.dropFirst() { context.addLine(to: point) } context.closePath() context.strokePath() } let labelRect = CGRect( x: convertedPoints[0].x, y: convertedPoints[0].y - 20, width: 200, height: 20 ) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .left let attributes: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 14), .foregroundColor: OverlayColor.yellow, .paragraphStyle: paragraphStyle, ] let attributedText = NSAttributedString( string: "\(format): \(text)", attributes: attributes) attributedText.draw(in: labelRect) } } private func convertToOverlayCoordinates(cameraPoint: CGPoint, overlaySize: CGSize) -> CGPoint { let cameraSize = cameraPreviewSize let scaleX = overlaySize.width / cameraSize.width let scaleY = overlaySize.height / cameraSize.height if scaleX < scaleY { let deltaX = CGFloat((cameraSize.width * scaleY - overlaySize.width) / 2) return CGPoint(x: cameraPoint.x * scaleY - deltaX, y: cameraPoint.y * scaleY) } else { let deltaY = CGFloat((cameraSize.height * scaleX - overlaySize.height) / 2) return CGPoint(x: cameraPoint.x * scaleX, y: cameraPoint.y * scaleX - deltaY) } } }Explanation
- The
drawmethod strokes the four corners of every barcode and writes the format and text above the first corner. convertToOverlayCoordinatesmaps camera pixel space into the overlay bounds, matching the aspect-fill gravity of the preview layer.
- The
-
In
CameraViewController.viewDidLoad, create the overlay above the preview layer:overlayView = BarcodeOverlayView() overlayView.wantsLayer = true overlayView.layer?.backgroundColor = NSColor.clear.cgColor overlayView.frame = view.bounds view.addSubview(overlayView, positioned: .above, relativeTo: nil)The Y flip is already applied inside the Objective-C++ wrapper (
height - points[i][1]), so the overlay draws the returned points as-is. No extra frame mirroring is needed.
Step 7: Run the macOS Barcode Scanner App
Run the macOS barcode scanner app in Xcode to see the results:

Common Issues and Edge Cases
- Barcode overlays appear flipped or offset: The C++ result uses a top-left origin while AppKit draws from the bottom-left, so the wrapper flips each Y value with
height - points[i][1]. If overlays still drift, verify thatcameraPreviewSizematches the actual pixel-buffer dimensions, not the display-point size. dylibnot found at runtime: Ensure the.dylibfiles are added in bothLink Binary With Librariesand aCopy Filesbuild phase targetingFrameworks. Without the copy phase, the app compiles but crashes on launch withLibrary not loaded.- Camera permission denied with no prompt: On macOS 14+, the camera entitlement must be present in the
.entitlementsfile and theInfo.plistmust includeNSCameraUsageDescription. Missing either one silently blocks access.