How to Build an iOS QR Code and Barcode Scanner with SwiftUI on Apple Silicon

SwiftUI is a robust UI framework designed for constructing native applications across all Apple platforms. This article will guide you through developing an iOS QR code scanner utilizing SwiftUI and the Dynamsoft Barcode Reader on an Apple Silicon Mac (M1/M2/M3/M4). The v11 barcode SDK is distributed as a single bundle (barcode-reader-spm) that you add to the project with Swift Package Manager — no CocoaPods, no Podfile, and no separate frameworks to link. One import DynamsoftCaptureVisionBundle gives you everything: the CaptureVisionRouter, the CameraEnhancer/CameraView camera stack, and the LicenseManager.

What you’ll build: A native iOS app that opens the camera, detects barcodes and QR codes in real time, and draws a highlighted quad plus the decoded text right on the preview as a per-barcode annotation — all built with SwiftUI and Dynamsoft Capture Vision.

Key Takeaways

  • SwiftUI apps can integrate a full-featured barcode and QR code scanner by wrapping Dynamsoft’s CameraView in a UIViewRepresentable struct.
  • Add the Dynamsoft Barcode Reader Bundle v11 to the project with Swift Package Manager (https://github.com/Dynamsoft/barcode-reader-spm, product DynamsoftBarcodeReader). The Capture Vision Bundle (router, camera enhancer, license) is resolved automatically as a package dependency.
  • A single import DynamsoftCaptureVisionBundle replaces the five module imports that older v10 samples needed (DynamsoftCore, DynamsoftLicense, DynamsoftBarcodeReader, DynamsoftCaptureVisionRouter, DynamsoftCameraEnhancer).
  • The CapturedResultReceiver protocol delivers decoded results asynchronously. Each decoded barcode is drawn as a quad on the SDK’s barcode drawing layer, and its text is drawn on a custom annotation layer that follows the barcode.
  • License verification failures are surfaced to the UI as a SwiftUI alert instead of failing silently.
  • This approach works on all Apple Silicon Macs (M1 through M4) and deploys to any iPhone or iPad running iOS 15.2 or later (the SDK itself supports iOS 13+).

Common Developer Questions

How do I scan QR codes from the camera in a SwiftUI iOS app?

Initialize Dynamsoft Camera Enhancer for the camera session, connect it to a CaptureVisionRouter, and implement CapturedResultReceiver so decoded barcodes are drawn on the camera view in real time. That is the core scanning loop used in this app.

How do I add the Dynamsoft barcode SDK to an iOS project?

Open Xcode, go to File > Add Package Dependencies…, paste https://github.com/Dynamsoft/barcode-reader-spm, and add the DynamsoftBarcodeReader product to your app target. Xcode resolves the package and downloads the SDK frameworks automatically on the first build. No Podfile or .xcworkspace is needed — open the .xcodeproj directly.

How do I wrap a UIKit camera view in SwiftUI with UIViewRepresentable?

Expose the SDK’s CameraView from a manager object, then host it inside SwiftUI through a UIViewRepresentable wrapper. The wrapper pins the CameraView to a container with Auto Layout constraints, so the overlay coordinates stay in sync with the preview across device sizes and rotations without manual frame management.

Demo Video: iOS SwiftUI Barcode and QR Code Scanner

Prerequisites

Before starting, ensure you have the following tools and resources:

  • Xcode: The integrated development environment (IDE) for macOS, required for iOS app development. Xcode 16 or later is recommended (the sample project uses the modern synchronized-folder project format).

  • A Trial License Key for the Dynamsoft iOS Barcode SDK. Get a 30-day free trial license to unlock the full capabilities of the SDK for development and testing. A new device with a network connection can also pick up a short automatic trial, but a real trial key is the reliable path for development.

Build an iOS QR Code Scanner with SwiftUI Step by Step

Dynamsoft Camera Enhancer provides a camera view that simplifies starting a camera session with just a few lines of Swift code. In the v11 SDK the barcode engine, the Capture Vision router, and the camera enhancer ship together, so the whole barcode reading pipeline — camera, decoding, and license — is driven from one import. The following sections will guide you through integrating it into a SwiftUI project step by step.

Step 1: Create the Project and Add the SDK with Swift Package Manager

  1. Create a new iOS SwiftUI app in Xcode and name it qrscanner.
  2. In Xcode, go to File > Add Package Dependencies… and paste the package URL:

     https://github.com/Dynamsoft/barcode-reader-spm
    
  3. Keep the default dependency rule Up to Next Major Version (from 11.6.2000 at the time of writing), click Add Package, then add the DynamsoftBarcodeReader product to the qrscanner app target.

    The package depends on Dynamsoft Capture Vision (DynamsoftCaptureVisionBundle), which Xcode resolves automatically. The first build downloads the SDK .xcframeworks from Dynamsoft’s CDN, so make sure you have a network connection.

If you clone the sample repository instead of building from scratch, the package is already wired up — just open the project:

open qrscanner.xcodeproj

Step 2: Configure Camera Access Permission

Navigate to TARGETS > Info tab in Xcode and add the key Privacy - Camera Usage Description:

Privacy - Camera Usage Description

iOS camera access permission

Step 3: Set the Dynamsoft Barcode Reader License Key

SwiftUI does not use an AppDelegate.swift file by default for app lifecycle management. However, you can integrate UIKit AppDelegate functionality using the App protocol and the @UIApplicationDelegateAdaptor property wrapper.

  1. Add an AppDelegate.swift file to your project and configure the license key within the application(_:didFinishLaunchingWithOptions:) method:

     import SwiftUI
     import UIKit
     import DynamsoftCaptureVisionBundle
    
     // Shared state that surfaces license verification errors as a SwiftUI alert.
     class LicenseState: ObservableObject {
         static let shared = LicenseState()
         @Published var isErrorPresented = false
         @Published var errorMessage = ""
     }
    
     class AppDelegate: UIResponder, UIApplicationDelegate, LicenseVerificationListener {
    
         func onLicenseVerified(_ isSuccess: Bool, error: Error?) {
             if !isSuccess {
                 let message = error?.localizedDescription ?? "Unknown error"
                 print("\(message)")
                 DispatchQueue.main.async {
                     LicenseState.shared.errorMessage =
                         "\(message)\n\nPlease check the license key in AppDelegate.swift."
                     LicenseState.shared.isErrorPresented = true
                 }
             }
         }
    
         func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
             LicenseManager.initLicense("LICENSE-KEY", verificationDelegate: self)
             return true
         }
     }
    

    Remember to replace LICENSE-KEY with your actual Dynamsoft Barcode Reader license key. LicenseState publishes the verification error so that ContentView can show an in-app alert (see Step 6) instead of failing silently when the key is missing or expired.

  2. In your qrscannerApp.swift file, utilize the @UIApplicationDelegateAdaptor to designate your AppDelegate class as the delegate for UIApplication:

     import SwiftUI
    
     @main
     struct qrscannerApp: App {
         @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
         var body: some Scene {
             WindowGroup {
                 ContentView()
             }
         }
     }
    

Step 4: Build the Camera Manager

Create a CameraManager.swift file. The class owns the three SDK objects that make up the pipeline: the CameraView (a UIView subclass that renders the preview and the drawing layers), the CameraEnhancer (dce), and the CaptureVisionRouter (cvr), which runs the barcode-reading task on every camera frame:

import Foundation
import UIKit
import DynamsoftCaptureVisionBundle

class CameraManager: NSObject, ObservableObject, CapturedResultReceiver {

    // Preset drawing layer for barcode results: 1 = DDN, 2 = DBR, 3 = DLR
    private let barcodeDrawingLayerId: UInt = 2
    private let labelHeight: UInt = 22
    private let labelWidth: UInt = 240
    private var labelDrawingLayerId: UInt = 0

    private var cameraView = CameraView()
    private let dce = CameraEnhancer()
    private let cvr = CaptureVisionRouter()

    override init() {
        super.init()
        setUpDCV()
    }

The setup method enables the barcode drawing layer and creates a dedicated layer for the text annotations. The annotation layer gets a drawing style — white text on a translucent dark background — so the labels stay readable on any camera scene:

    func setUpDCV() {
        // Show the barcode drawing layer so decoded results are drawn on the camera view.
        cameraView.getDrawingLayer(barcodeDrawingLayerId)?.visible = true

        // A custom layer that holds one text annotation per decoded barcode.
        let labelStyleId = DrawingStyleManager.createDrawingStyle(
            UIColor.black.withAlphaComponent(0.55),
            strokeWidth: 1,
            fill: UIColor.black.withAlphaComponent(0.55),
            textColor: .white,
            font: .systemFont(ofSize: 12)
        )
        let labelLayer = cameraView.createDrawingLayer()
        labelLayer.visible = true
        labelLayer.setDefaultStyle(labelStyleId)
        labelDrawingLayerId = labelLayer.layerId

        dce.cameraView = cameraView
        // Set the camera enhancer as the input.
        try! cvr.setInput(dce)
        // Add CapturedResultReceiver to receive the result callback when a video frame is processed.
        cvr.addResultReceiver(self)
    }

    func getCameraView() -> CameraView {
        return cameraView
    }

When a frame contains decoded barcodes, the receiver callback clears both layers and redraws them: a quad around every barcode plus a text label anchored to it. The label is placed above the quad when there is room, and falls back to below its top edge when the barcode is close to the top of the screen:

    func onDecodedBarcodesReceived(_ result: DecodedBarcodesResult) {
        guard let items = result.items, items.count > 0,
              let barcodeLayer = cameraView.getDrawingLayer(barcodeDrawingLayerId),
              let labelLayer = cameraView.getDrawingLayer(labelDrawingLayerId) else {
            return
        }

        barcodeLayer.clearDrawingItems()
        labelLayer.clearDrawingItems()

        for item in items {
            barcodeLayer.addDrawingItems([QuadDrawingItem(quadrilateral: item.location)])

            // Place the text annotation above the barcode quad.
            guard let points = item.location.points as? [CGPoint], points.count == 4 else { continue }
            let minX = points.map { $0.x }.min() ?? 0
            let minY = points.map { $0.y }.min() ?? 0
            let gap: CGFloat = 6
            var y = minY - CGFloat(labelHeight) - gap
            if y < 2 {
                y = minY + gap
            }
            let labelItem = TextDrawingItem(
                text: item.text,
                topLeftPoint: CGPoint(x: minX, y: y),
                width: labelWidth,
                height: labelHeight
            )
            labelLayer.addDrawingItems([labelItem])
        }
    }

Finally, manage the view lifecycle to switch the camera session on and off:

    func viewDidAppear() {
        dce.open()
        cvr.startCapturing(PresetTemplate.readBarcodes.rawValue) { isSuccess, error in
            if (!isSuccess) {
                if let error = error {
                    print(error.localizedDescription)
                }
            }
        }
    }

    func viewDidDisappear() {
        dce.close()
        cvr.stopCapturing()
    }
}

Step 5: Create a SwiftUI Camera View for QR Code Scanning

Since CameraView is a UIView subclass, it needs to be wrapped in a UIViewRepresentable struct for use in SwiftUI. The wrapper returns a container and pins the CameraView to it with Auto Layout. The camera preview then always fills the space SwiftUI gives it, and the overlay drawn by the SDK stays aligned with the preview even when the device rotates or the layout changes:

import Foundation
import SwiftUI
import DynamsoftCaptureVisionBundle

struct DynamsoftCameraView: UIViewRepresentable {
    var cameraManager: CameraManager

    func makeUIView(context: Context) -> UIView {
        let container = UIView()
        container.backgroundColor = .black

        let cameraView = cameraManager.getCameraView()
        cameraView.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(cameraView)
        NSLayoutConstraint.activate([
            cameraView.topAnchor.constraint(equalTo: container.topAnchor),
            cameraView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
            cameraView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            cameraView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
        ])
        return container
    }

    func updateUIView(_ uiView: UIView, context: Context) {

    }
}

Step 6: Construct the SwiftUI View Hierarchy

In the ContentView.swift file, create a ContentView struct containing a DynamsoftCameraView instance. The camera fills the whole screen, with a small title on top. The .alert modifier observes LicenseState and pops up an alert if license verification fails:

import SwiftUI

struct ContentView: View {
    @ObservedObject private var cameraManager = CameraManager()
    @ObservedObject private var licenseState = LicenseState.shared

    var body: some View {
        ZStack {
            DynamsoftCameraView(cameraManager: cameraManager)
                .ignoresSafeArea()
                .onAppear() {
                    cameraManager.viewDidAppear()
                }.onDisappear(){
                    cameraManager.viewDidDisappear()
                }

            VStack {
                Text("iOS QR Code Scanner")
                    .font(.title)
                    .foregroundColor(.orange)
                    .padding(.top, 8)

                Spacer()
            }
        }
        .alert("License Error", isPresented: $licenseState.isErrorPresented) {
            Button("OK", role: .cancel) {}
        } message: {
            Text(licenseState.errorMessage)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Step 7: Deploy and Test the Scanner on iPhone or iPad

Configure the signing settings for your Xcode project (select your development team on the Signing & Capabilities tab of the target), then deploy and test the app on a physical device. Grant the camera permission when the prompt appears. Scan a QR code or a barcode: you should see a quad drawn around it and the decoded text in the annotation right above it.

ios swiftui camera qr code scanner

Common Issues and Edge Cases

  • Camera permission denied at runtime: If the user taps “Don’t Allow” on the camera prompt, the preview stays black with no error. Check AVCaptureDevice.authorizationStatus(for: .video) and show an alert directing the user to Settings > Privacy > Camera.
  • First build downloads the SDK: Xcode fetches the Dynamsoft .xcframeworks from the network the first time it resolves the package. If the build fails with a download error, check your connection and try File > Packages > Reset Package Caches.
  • License errors are silent in the console but not on screen: If scanning returns nothing, look for the in-app “License Error” alert first — the sample raises it whenever onLicenseVerified reports a failure (for example when LICENSE-KEY was not replaced). The error details are also printed to the Xcode console.
  • Overlay alignment on different screen sizes: The CameraView is pinned with Auto Layout inside the UIViewRepresentable container (Step 5), so the SDK keeps the overlay coordinates in sync with the preview on any device size and rotation. Do not set the view frame from UIScreen.main.bounds at init time.

Source Code

Get the complete sample project source code on GitHub