How to Create a SwiftUI Barcode Scanner Project for macOS and iOS

In the previous tutorial, we explored how to build a SwiftUI barcode scanner app for macOS using the Dynamsoft Capture Vision C++ SDK. Since SwiftUI is a cross-platform framework supporting both macOS and iOS, and the Dynamsoft Capture Vision SDK provides a Swift Package Manager (SPM) for iOS, it is theoretically possible to make the SwiftUI project compatible with both platforms. This article demonstrates how to modify the existing macOS barcode scanner project to support iOS.

iOS 1D/2D Barcode Scanner Demo Video

Prerequisites

  • License Key: Obtain a valid license key for the Dynamsoft Capture Vision SDK.
  • capture-vision-spm: The Swift Package Manager (SPM) for the Dynamsoft Capture Vision SDK.

Configuring SwiftUI Barcode Scanner Project for macOS and iOS

  1. In Xcode, select File > Add Package Dependencies to add the capture-vision-spm package:

    capture vision swift package manager

  2. After adding all packages to the target, navigate to Project > Build Phases > Link Binary With Libraries, and adjust the supported platform of the packages to iOS:

    link binary with libraries

  3. For macOS-specific bridging code that is unnecessary for iOS, ensure the build passes by conditionally excluding the macOS code using macros in *.h and *.mm files:

     #include <TargetConditionals.h>
        
     #if defined(__APPLE__) && defined(__MACH__) && !TARGET_OS_IPHONE
        
     // macOS code
        
     #endif
    
  4. To access the camera on iOS, go to Project > Info and add the NSCameraUsageDescription key:

    ios camera usage description

SwiftUI Barcode Scanner for iOS

The naming conventions for macOS and iOS differ. For example, NSImage is used on macOS, while UIImage is used on iOS.

Below is a comparison table of classes and methods used in macOS and iOS barcode scanner projects:

macOS (Swift) iOS (Swift)
NSImage UIImage
NSView UIView
NSColor UIColor
NSViewControllerRepresentable UIViewControllerRepresentable
NSGraphicsContext UIGraphicsGetCurrentContext
NSFont UIFont
NSViewController UIViewController
viewDidLayout viewDidLayoutSubviews

To make the code compatible with both platforms, we can use #if os(macOS) and #if os(iOS) to conditionally compile platform-specific code.

Constructing the SwiftUI Camera View for iOS

In CameraView.swift, implement the CameraView struct using UIViewControllerRepresentable for iOS:

#if os(iOS)
    struct CameraView: UIViewControllerRepresentable {
        @Binding var image: ImageType?
        @Binding var shouldCapturePhoto: Bool

        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }

        func makeUIViewController(context: Context) -> CameraViewController {
            let cameraViewController = CameraViewController()
            context.coordinator.cameraViewController = cameraViewController
            return cameraViewController
        }

        func updateUIViewController(_ uiViewController: CameraViewController, context: Context) {
            
        }

        class Coordinator: NSObject {
            var parent: CameraView
            var cameraViewController: CameraViewController?

            init(_ parent: CameraView) {
                self.parent = parent
            }
        }
    }
#elseif os(macOS)
    struct CameraView: NSViewControllerRepresentable {
        ...
    }
#endif

Importing the Dynamsoft Capture Vision SDK for iOS

In CameraViewController.swift, import the Dynamsoft Capture Vision SDK for iOS as follows:

#if os(iOS)
    import UIKit
    import CoreGraphics
    import DynamsoftCaptureVisionRouter
    import DynamsoftBarcodeReader
    import DynamsoftLicense
    typealias ViewController = UIViewController
    typealias ImageType = UIImage
#elseif os(macOS)
    import Cocoa
    typealias ViewController = NSViewController
    typealias ImageType = NSImage
#endif

Setting the License Key for the Dynamsoft Capture Vision SDK

The license setup method differs between macOS (synchronous) and iOS (asynchronous). Modify the code in CameraViewController.swift as follows:

class CameraViewController: ViewController, AVCapturePhotoCaptureDelegate,
    AVCaptureVideoDataOutputSampleBufferDelegate
{
    override func viewDidLoad() {
        super.viewDidLoad()

        let licenseKey =
            "LICENSE-KEY"

        #if os(iOS)
            setLicense(license: licenseKey)
        #elseif os(macOS)
            let result = CaptureVisionWrapper.initializeLicense(licenseKey)
            if result == 0 {
                print("License initialized successfully")
            } else {
                print("Failed to initialize license with error code: \(result)")
            }
        #endif
        ...
    }
}

#if os(iOS)
    extension CameraViewController: LicenseVerificationListener {

        func onLicenseVerified(_ isSuccess: Bool, error: Error?) {
            if !isSuccess {
                if let error = error {
                    print("\(error.localizedDescription)")
                }
            }
        }

        func setLicense(license: String) {
            LicenseManager.initLicense(license, verificationDelegate: self)
        }
    }
#endif

Decoding Barcodes on iOS

The barcode decoding methods for macOS and iOS differ slightly. For macOS, you pass width, height, stride, and format directly to the capture method. On iOS, an ImageData object is created and passed to the capture method:

#if os(iOS)
    let cvr = CaptureVisionRouter()

    let buffer = Data(bytes: baseAddress, count: bytesPerRow * height)
    let imageData = ImageData(
        bytes: buffer, width: UInt(width), height: UInt(height),
        stride: UInt(bytesPerRow), format: .ARGB8888, orientation: 0, tag: nil)
    let result = cvr.captureFromBuffer(
        imageData, templateName: PresetTemplate.readBarcodes.rawValue)
#elseif os(macOS)
    let cv = CaptureVisionWrapper()

    ...
    let buffer = Data(bytes: baseAddress, count: bytesPerRow * height)
    let barcodeArray =
        cv.captureImage(
            with: buffer, width: Int32(width), height: Int32(Int(height)),
            stride: Int32(Int(bytesPerRow)), pixelFormat: pixelFormat)
        as? [[String: Any]] ?? []
#endif

Converting Barcode Coordinates for iOS

On iOS, when scanning barcodes in portrait mode, the image is rotated 90 degrees. Adjust coordinates as follows:

#if os(iOS)
    private func convertToOverlayCoordinates(
        cameraPoint: CGPoint, overlaySize: CGSize, orientation: AVCaptureVideoOrientation
    ) -> CGPoint {
        let cameraSize = cameraPreviewSize

        let scaleX = overlaySize.width / cameraSize.height
        let scaleY = overlaySize.height / cameraSize.width

        var transformedPoint = CGPoint.zero

        if scaleX < scaleY {
            let deltaX = CGFloat((cameraSize.height * scaleY - overlaySize.width) / 2)

            transformedPoint = CGPoint(
                x: cameraPoint.x * scaleY, y: cameraPoint.y * scaleY)

            transformedPoint = CGPoint(
                x: overlaySize.width - transformedPoint.y + deltaX, y: transformedPoint.x)

        } else {
            let deltaY = CGFloat((cameraSize.width * scaleX - overlaySize.height) / 2)
            transformedPoint = CGPoint(
                x: cameraPoint.x * scaleX, y: cameraPoint.y * scaleX)

            transformedPoint = CGPoint(
                x: overlaySize.width - transformedPoint.y, y: transformedPoint.x - deltaY)
        }

        return transformedPoint

    }
#elseif os(macOS)
    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)
        }
    }
#endif

Running the iOS Barcode Scanner App

  1. Select a target device in Xcode:

    select ios device

  2. Run the barcode scanner app to see the results:

    iOS barcode scanner in SwiftUI

Source Code

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