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.
This article is Part 4 in a 5-Part Series.
- Part 1 - Creating an iOS Barcode and QR Code Scanner with SwiftUI on M1 Mac
- Part 2 - How to Build an iOS MRZ Scanner with SwiftUI and Dynamsoft Capture Vision
- Part 3 - How to Build a macOS Barcode Scanner App Using SwiftUI and C++ Barcode SDK from Scratch
- Part 4 - How to Create a SwiftUI Barcode Scanner Project for macOS and iOS
- Part 5 - How to Build a Document Scanner App with SwiftUI for Both macOS and 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
-
In Xcode, select
File > Add Package Dependencies
to add thecapture-vision-spm
package: -
After adding all packages to the target, navigate to
Project > Build Phases > Link Binary With Libraries
, and adjust the supported platform of the packages toiOS
: -
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
-
To access the camera on iOS, go to
Project > Info
and add theNSCameraUsageDescription
key:
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
-
Select a target device in Xcode:
-
Run the barcode scanner app to see the results: