How to Build a macOS Framework Wrapping C++ in Objective-C++ for Swift Barcode Scanning

Dynamsoft currently provides only a C++ Barcode SDK for macOS. Previously, we built a macOS barcode scanner by combining Swift, Objective-C and C++. Since modern macOS app development primarily relies on Swift, creating a macOS framework that wraps the C++ Barcode SDK with Objective-C++ can streamline the process of calling C++ APIs from Swift. In this article, we will extract the Objective-C++ and C++ code from the macOS barcode scanner project and move them into a dedicated macOS framework project. The original SwiftUI project will then use this macOS framework to handle barcode detection.

What you’ll build: A reusable macOS framework that wraps the Dynamsoft C++ Barcode SDK in Objective-C++, letting you call barcode-detection APIs directly from Swift or SwiftUI with no C++ boilerplate in your app code.

Key Takeaways

  • You can bridge any C++ SDK to Swift on macOS by packaging it as a framework using Objective-C++ (.mm) files as the language boundary.
  • The Dynamsoft C++ Barcode SDK decodes QR codes, Code 128, DataMatrix, PDF417, and 20+ other formats via a single CCaptureVisionRouter::Capture() call.
  • Wrapping the SDK in a dedicated Xcode framework project decouples your Swift app from C++ compilation, enabling clean CocoaPods distribution.
  • This Objective-C++ wrapper pattern applies to any macOS app that needs high-throughput barcode scanning from a live camera stream or static image files.

Common Developer Questions

  • How do I wrap a C++ SDK in a macOS framework so I can call it from Swift?
  • How do I build an Objective-C++ macOS framework that bridges C++ and SwiftUI?
  • How do I publish a macOS framework containing a C++ library to CocoaPods?

macOS SwiftUI Barcode Scanner Demo Video

Prerequisites

Step 1: Create a macOS Framework Project Wrapping C++ in Objective-C++

  1. Scaffold a new macOS framework project in Xcode:

    macOS Framework Project

  2. Drag the header and library files from the Dynamsoft Barcode Reader C++ SDK into the project:

    macOS Framework project with .h and .dylib

    Next, navigate to Build Phases > Copy Files and add all the .dylib files to the Frameworks section.

  3. In the public header file (DCV.h), define the required enum types and expose essential Objective-C++ APIs:

     #ifndef DCV_H
     #define DCV_H
        
     #import <Foundation/Foundation.h>
        
     FOUNDATION_EXPORT double DCVVersionNumber;
        
     FOUNDATION_EXPORT const unsigned char DCVVersionString[];
        
     typedef NS_ENUM(NSInteger, PixelFormat) {
         ...
     };
     typedef NS_ENUM(unsigned long long, BarcodeType) {
         ...
     };
        
     @interface CaptureVisionWrapper : NSObject
     + (int)initializeLicense:(NSString *)licenseKey;
        
     - (NSArray *)decodeBufferWithData:(void *)baseAddress
                                 width:(int)width
                                height:(int)height
                                stride:(int)stride
                           pixelFormat:(PixelFormat)pixelFormat;
        
     - (NSArray *)decodeFileWithPath:(NSString *)filePath;
     - (NSString *)getSettings;
     - (int)setSettings:(NSString *)jsonString;
     - (int)setBarcodeFormats:(unsigned long long)formats;
     @end
        
     #endif
    
    
  4. Create a dcv.mm file to implement the APIs by calling the underlying C++ SDK methods:

     #import <CoreVideo/CoreVideo.h>
     #import <Foundation/Foundation.h>
    
     #import "DCV.h"
    
     #ifdef __cplusplus
     #include "DynamsoftCaptureVisionRouter.h"
     #include "DynamsoftUtility.h"
     #include "template.h"
    
     using namespace dynamsoft::license;
     using namespace dynamsoft::cvr;
     using namespace dynamsoft::dbr;
     using namespace dynamsoft::utility;
     using namespace dynamsoft::basic_structures;
    
     #endif 
    
     @implementation CaptureVisionWrapper {
         CCaptureVisionRouter *cvr; 
     }
    
     + (int)initializeLicense:(NSString *)licenseKey {
         char errorMsgBuffer[512] = {0}; 
         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;
     }
    
     - (instancetype)init {
         self = [super init];
         if (self) {
             try {
             cvr = new CCaptureVisionRouter(); // Initialize the C++ object
        
             char errorMsgBuffer[512] = {0};
             int ret = cvr->InitSettings(jsonString.c_str(), errorMsgBuffer,
                                         sizeof(errorMsgBuffer));
        
             if (ret != 0) {
                 NSString *errorMessage = [NSString stringWithUTF8String:errorMsgBuffer];
                 NSLog(@"Init setting failed: %@", errorMessage);
             }
             } catch (const std::exception &ex) {
             NSLog(@"Exception during initialization: %s", ex.what());
             } catch (...) {
             NSLog(@"Unknown exception during initialization");
             }
         }
         return self;
     }
    
     - (NSArray *)decodeBufferWithData:(void *)baseAddress
                                 width:(int)width
                             height:(int)height
                             stride:(int)stride
                         pixelFormat:(PixelFormat)pixelFormat {
         if (!baseAddress) {
             NSLog(@"Error: baseAddress is null");
             return nil;
         }
        
         NSArray *results = nil;
        
         try {
             CImageData *imageStruct = new CImageData(
                 stride * height, (unsigned char *)baseAddress, width, height, stride,
                 static_cast<ImagePixelFormat>(pixelFormat));
    
             CCapturedResult *result = cvr->Capture(imageStruct, "");
             results = [self wrapResults:result];
             delete imageStruct;
         } catch (const std::exception &ex) {
             NSLog(@"Exception during captureImageWithData: %s", ex.what());
         } catch (...) {
             NSLog(@"Unknown exception during captureImageWithData");
         }
        
         return results;
     }
    
     - (NSArray *)decodeFileWithPath:(NSString *)filePath {
         if (!filePath) {
             NSLog(@"Error: filePath is null");
             return nil;
         }
        
         NSArray *results = nil;
        
         try {
             const char *fileCString = [filePath UTF8String];
             CCapturedResult *result = cvr->Capture(fileCString, "");
             results = [self wrapResults:result];
         } catch (const std::exception &ex) {
             NSLog(@"Exception during captureImageWithFilePath: %s", ex.what());
         } catch (...) {
             NSLog(@"Unknown exception during captureImageWithFilePath");
         }
        
         return results;
     }
    
     - (NSArray *)wrapResults:(CCapturedResult *)result {
         if (!result) {
             NSLog(@"Error: result is null");
             return nil;
         }
        
         NSMutableArray *barcodeArray = [NSMutableArray array];
        
         try {
             CDecodedBarcodesResult *barcodeResult = result->GetDecodedBarcodesResult();
             if (!barcodeResult || barcodeResult->GetItemsCount() == 0) {
             return nil;
             }
        
             int barcodeResultItemCount = barcodeResult->GetItemsCount();
        
             for (int j = 0; j < barcodeResultItemCount; j++) {
             const CBarcodeResultItem *barcodeResultItem = barcodeResult->GetItem(j);
             const char *format = barcodeResultItem->GetFormatString();
             const char *text = barcodeResultItem->GetText();
             int angle = barcodeResultItem->GetAngle();
             CPoint *points = barcodeResultItem->GetLocation().points;
             unsigned char *raw = barcodeResultItem->GetBytes();
        
             NSDictionary *barcodeData = @{
                 @"format" : format ? [NSString stringWithUTF8String:format] : @"",
                 @"text" : text ? [NSString stringWithUTF8String:text] : @"",
                 @"angle" : @(angle),
                 @"barcodeBytes" :
                     [NSData dataWithBytes:raw
                                 length:barcodeResultItem->GetBytesLength()],
                 @"points" : @[
                 @{@"x" : @(points[0][0]), @"y" : @(points[0][1])},
                 @{@"x" : @(points[1][0]), @"y" : @(points[1][1])},
                 @{@"x" : @(points[2][0]), @"y" : @(points[2][1])},
                 @{@"x" : @(points[3][0]), @"y" : @(points[3][1])}
                 ]
             };
        
             [barcodeArray addObject:barcodeData];
             }
        
             barcodeResult->Release();
             result->Release();
         } catch (const std::exception &ex) {
             NSLog(@"Exception during wrapResults: %s", ex.what());
         } catch (...) {
             NSLog(@"Unknown exception during wrapResults");
         }
        
         return [barcodeArray copy];
     }
    
     - (void)dealloc {
         if (cvr) {
             delete cvr;
             cvr = nullptr;
         }
     }
    
     - (NSString *)getSettings {
         char *tpl = cvr->OutputSettings("");
         NSString *settings = [NSString stringWithUTF8String:tpl];
         dynamsoft::cvr::CCaptureVisionRouter::FreeString(tpl);
         return settings;
     }
    
     - (int)setSettings:(NSString *)json {
         char *tpl = (char *)[json UTF8String];
         char errorMessage[256];
         int ret = cvr->InitSettings(tpl, errorMessage, 256);
         if (ret != 0) {
             NSLog(@"Set settings failed: %s", errorMessage);
         }
         return ret;
     }
    
     - (int)setBarcodeFormats:(unsigned long long)formats {
         SimplifiedCaptureVisionSettings pSettings = {};
         cvr->GetSimplifiedSettings("", &pSettings);
         pSettings.barcodeSettings.barcodeFormatIds = formats;
        
         char szErrorMsgBuffer[256];
         int ret = cvr->UpdateSettings("", &pSettings, szErrorMsgBuffer, 256);
         if (ret != 0) {
             NSLog(@"Set barcode formats failed: %s", szErrorMsgBuffer);
         }
         return ret;
     }
     @end
    
  5. Edit the scheme to use the Release build configuration for the framework:

    macOS Framework Scheme for release build

  6. Build the framework project, then locate the generated DCV.framework by navigating to Product > Show Build Folder in Finder:

    macOS Framework Build Success

Step 2: Integrate the DCV Framework into a SwiftUI Project

  1. Start a new macOS SwiftUI project in Xcode:

    macOS SwiftUI Project

  2. Copy ImageViewer.swift, ContentView.swift, CaptureVisionApp.swift, CameraViewController.swift, CameraView.swift and CameraView.swift from the original SwiftUI project to the new project.
  3. Disable the App Sandbox capability in the entitlements file.

     <key>com.apple.security.app-sandbox</key>
     <false/>
    
  4. Add DCV.framework to the project by selecting File > Add Files. Ensure the Embed & Sign option is selected in the Frameworks, Libraries, and Embedded Content section.

    macOS SwiftUI Project with DCV.framework

  5. In CameraViewController.swift, modify the code logic to utilize the framework:

     import DCV
        
     override func viewDidLoad() {
         super.viewDidLoad()
    
         // Initialize the license here
         // Get a trial license key from: https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform
         let licenseKey =
             "LICENSE-KEY"
    
         let result = CaptureVisionWrapper.initializeLicense(licenseKey)
    
         if result == 0 {
             print("License initialized successfully")
         } else {
             print("Failed to initialize license with error code: \(result)")
         }
    
         ...
     }
    
     func processCameraFrame(_ pixelBuffer: CVPixelBuffer) {
         let previewWidth = CVPixelBufferGetWidth(pixelBuffer)
         let previewHeight = CVPixelBufferGetHeight(pixelBuffer)
    
         DispatchQueue.main.async { [weak self] in
             guard let self = self else { return }
             self.overlayView.cameraPreviewSize = CGSize(width: previewWidth, height: previewHeight)
         }
    
         CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
    
         let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
         let width = CVPixelBufferGetWidth(pixelBuffer)
         let height = CVPixelBufferGetHeight(pixelBuffer)
         let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
         let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
    
         if let baseAddress = baseAddress {
             let barcodeArray =
                 cv.decodeBuffer(
                     withData: baseAddress, width: Int32(width), height: Int32(Int(height)),
                     stride: Int32(Int(bytesPerRow)), pixelFormat: PixelFormat.ARGB8888)
                 as? [[String: Any]] ?? []
    
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
                 self.overlayView.barcodeData = barcodeArray
                 self.overlayView.setNeedsDisplay(self.overlayView.bounds) 
             }
         }
    
         CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
    
     }
    

    You need to replace the LICENSE-KEY with your own license key.

  6. Build and run the macOS SwiftUI project to see the barcode scanner in action:

    macos framework for barcode scanning in SwiftUI

Step 3: Publish the macOS Framework to CocoaPods

After successfully building the macOS framework, you can publish it to CocoaPods for easy integration into other macOS projects.

  1. Compress the DCV.framework into a .zip file and upload it to a file hosting service, such as GitHub.
  2. Create a CocoaPods podspec file named DCV.podspec:

     Pod::Spec.new do |s|
     s.name             = 'DCV'
     s.version          = '1.0.1'
     s.summary          = 'macOS barcode reading framework.'
      
     s.description      = <<-DESC
     DCV is a versatile framework designed for barcode reading. It is built with Dynamsoft Barcode Reader C++ SDK. The framework supports various barcode types, including Code 39, Code 128, QR Code, DataMatrix, PDF417, etc.
     DESC
      
     s.homepage         = 'https://github.com/yushulx/ios-swiftui-barcode-mrz-document-scanner/tree/main/examples/macos_framework'
     s.license = { :type => 'MIT', :file => File.expand_path('LICENSE') }
     s.author           = { 'yushulx' => 'lingxiao1002@gmail.com' }
     s.source           = { :http => 'https://github.com/yushulx/ios-swiftui-barcode-mrz-document-scanner/raw/refs/heads/main/examples/macos_framework/dist/DCV.framework.zip' }
      
     s.macos.deployment_target = '10.13'
     s.vendored_frameworks = 'DCV.framework'
      
     s.frameworks = ['Foundation']
     s.requires_arc = true
      end
      
    

    Explanation:

    • The source field specifies the URL of the .zip file containing the framework.
    • The vendored_frameworks field specifies the prebuilt frameworks that are bundled with your plugin and should be included in the consuming project.
    • The frameworks field specifies the dependencies of the plugin.
    • The framework and license file should be placed in the same directory as the podspec file.
  3. Validate the podspec file:

     pod lib lint DCV.podspec
    
  4. Register a CocoaPods account if you don’t already have one:

     pod trunk register your_email@example.com 'Your Name'
    
  5. Publish the pod:

     pod trunk push DCV.podspec
    
  6. Run pod search DCV to verify if the pod has been successfully published. If you have received an approval email but the pod is not yet visible, run pod repo add master https://github.com/CocoaPods/Specs to manually adds the CocoaPods “master” spec repository to your local CocoaPods configuration.

Common Issues & Edge Cases

  • Linker errors with .dylib files at runtime: macOS requires .dylib files to be embedded in the app bundle’s Frameworks directory. If the app crashes with dyld: Library not loaded, verify that all .dylib files are listed in Build Phases > Copy Files with the Frameworks destination and that the framework’s Embed & Sign option is selected.
  • InitLicense returns a non-zero error code: The Dynamsoft license requires a valid internet connection for first-time online validation. Ensure network access is available and the license string matches the licensed product (Barcode Reader). Check the errorMsgBuffer value logged to the console for details.
  • App Sandbox blocks camera access or crashes on launch: Confirm that NSCameraUsageDescription is set in Info.plist and that the App Sandbox entitlement is either disabled (com.apple.security.app-sandbox = false) or has com.apple.security.device.camera enabled in the .entitlements file.

Source Code

https://github.com/yushulx/ios-swiftui-barcode-mrz-document-scanner/tree/main/examples/macos_framework