Implementing a Flutter QR Code Scanner with Swift, AVFoundation, and Dynamsoft Barcode Reader Bundle

In the previous article, we implemented a Flutter barcode and QR code scanner for Android using Kotlin and CameraX. Since the Dart code is platform-independent, no changes are necessary. In this article, we will take steps to implement the native camera and barcode scanning logic for iOS using Swift, AVFoundation, and the Dynamsoft Barcode Reader Bundle SDK.

What you’ll build: A Flutter iOS app that renders a full-screen AVFoundation camera preview through a Texture widget, decodes barcodes and QR codes in real time from CMSampleBuffer frames using the Dynamsoft Barcode Reader Bundle’s CaptureVisionRouter API in Swift, and overlays the barcode text and quadrilateral coordinates on the preview in Dart.

Key Takeaways

  • Rendering the camera preview in native Swift via a Flutter Texture and displaying it in Dart with a Texture widget avoids the per-frame memory copies between native code and Dart that make camera plugins slow for image processing.
  • AVFoundation AVCaptureVideoDataOutput frames are decoded on a background thread by wrapping the pixel buffer in an ImageData object and calling CaptureVisionRouter.captureFromBuffer(_:templateName:) with the PresetTemplate.readBarcodes template.
  • The legacy DynamsoftBarcodeReader 9.x pod and its BarcodeReader.decodeBuffer() / iImageData API have been superseded by the DynamsoftBarcodeReaderBundle pod (v11.x), which contains the same barcode engine as Dynamsoft Capture Vision and is initialized with LicenseManager.initLicense().
  • Barcode results cross the Swift–Dart boundary through a MethodChannel as a list of dictionaries (format, coordinates, angle, barcodeBytes), and the Dart side draws the overlay while the native side filters the decoded items by CapturedResultItemType.barcode.
  • The same architecture lets you swap barcode decoding for any custom image-processing algorithm while keeping the app UI in Dart.

Common Developer Questions

Why not use a Flutter camera plugin for real-time barcode scanning?

The camera plugin streams every frame to Dart through a platform channel, which copies the frame data across the native–Dart boundary on each frame and drives up memory usage and latency. Implementing the preview and the decoding in Swift with AVFoundation keeps the frames native and passes only the decoded barcode results to Dart.

Which Dynamsoft dependency should I use for iOS barcode scanning?

Use pod 'DynamsoftBarcodeReaderBundle' from CocoaPods (v11.x). It is the current lightweight barcode-only bundle that replaces the legacy DynamsoftBarcodeReader 9.x pod and exposes the CaptureVisionRouter API used in this tutorial. Installing it also pulls in the DynamsoftCaptureVisionBundle pod which contains the shared Capture Vision modules.

How do I decode an AVFoundation frame with the Capture Vision Router?

Copy the CVPixelBuffer base address into a Data object, fill an ImageData object with the bytes, width, height, stride, ImagePixelFormat.ARGB8888 format, and orientation, then call cvr.captureFromBuffer(imageData, templateName: PresetTemplate.readBarcodes.rawValue). The returned CapturedResult contains BarcodeResultItem entries carrying the barcode format string, text, location points, angle, and raw bytes.

Why is the deployment target raised to iOS 13.0?

The Dynamsoft Barcode Reader Bundle 11.x requires a minimum iOS deployment target of 13.0. When the pod is added, CocoaPods rejects the default iOS 12.0 target, so the Podfile declares platform :ios, '13.0' and the Xcode project’s IPHONEOS_DEPLOYMENT_TARGET is updated accordingly.

Prerequisites

Step 1: Installing Dynamsoft Barcode Reader Bundle for iOS

We use CocoaPods to install the Dynamsoft Barcode Reader Bundle SDK for iOS. If you haven’t installed CocoaPods yet, please follow the official instructions here to do so.

Once CocoaPods is ready, create a Podfile in the iOS folder of your Flutter project:

cd ios
pod init

Next, edit the Podfile to include the Dynamsoft Barcode Reader Bundle SDK and declare the minimum iOS version of 13.0:

platform :ios, '13.0'

target 'Runner' do
  use_frameworks!

  pod 'DynamsoftBarcodeReaderBundle','11.6.2000'

end

Save the Podfile and run pod install. This command will install or update the CocoaPods dependencies, including the Flutter framework required for your iOS project. The DynamsoftCaptureVisionBundle pod, which contains the Capture Vision modules shared across Dynamsoft SDKs, is installed automatically.

Note: The legacy DynamsoftBarcodeReader 9.x pod has been superseded by DynamsoftBarcodeReaderBundle (v11.x), which contains the same barcode engine as Dynamsoft Capture Vision and exposes the CaptureVisionRouter API.

Also, update the iOS deployment target of the Runner project to 13.0 in Xcode (or in the Runner.xcodeproj/project.pbxproj file). Otherwise, you may get the “DynamsoftBarcodeReaderBundle requires a higher minimum deployment target” error when running pod install.

Step 2: Adding Camera Permission to Info.plist

To enable camera access on iOS, open the Info.plist file located in the ios/Runner folder. Add the following keys:

<key>NSCameraUsageDescription</key>
<string>your usage description here</string>
<key>NSMicrophoneUsageDescription</key>
<string>your usage description here</string>

Step 3: Implementing Camera Preview with Flutter Texture in Swift

The Runner/AppDelegate.swift file serves as the entry point of the Flutter application. By default, it contains the following boilerplate code:

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

To integrate the camera functionality effectively, you’ll need to enhance the application method with the following steps:

  1. Establish a Flutter method channel. This is crucial for seamless communication between the Dart environment and Swift, allowing commands and data to be exchanged between the Flutter UI and native code.
  2. Implement a startCamera() method. This method should initiate the camera preview and continuously render this preview into a Flutter texture. This involves setting up the camera capture session, configuring input and output, and linking the camera output to a Flutter texture that can be displayed in the UI.

Flutter Method Channel in Swift

The Flutter method channel is a named channel that facilitates the sending of data between Dart and platform-specific code.

private let CHANNEL = "barcode_scan"
private var channel: FlutterMethodChannel?
private var width = 1920
private var height = 1080

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    guard let flutterViewController = window?.rootViewController as? FlutterViewController else {
      return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    flutterTextureEntry = flutterViewController.engine.textureRegistry

    channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: flutterViewController.binaryMessenger)
    channel?.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if call.method == "startCamera" {
        self.startCamera(result: result)
      } else if call.method == "getPreviewWidth" {
        result(self.width)
      } else if call.method == "getPreviewHeight" {
        result(self.height)
      }

      else {
        result(FlutterMethodNotImplemented)
      }
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  • The channel variable is an instance of FlutterMethodChannel. It receives method calls from Dart using the setMethodCallHandler method. Additionally, the invokeMethod method is used to send data from Swift to Dart.
  • The width and height variables, which store the camera preview size, are hardcoded to 1920x1080 here. The methods getPreviewWidth and getPreviewHeight retrieve and return these values to Dart, respectively.

Creating Flutter Texture and Camera Preview

Define the CustomCameraTexture class that extends NSObject and implements the FlutterTexture protocol:

class CustomCameraTexture: NSObject, FlutterTexture {
  private weak var textureRegistry: FlutterTextureRegistry?
  var textureId: Int64?
  private var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
  private let bufferQueue = DispatchQueue(label: "com.example.flutter/barcode_scan")
  private var _lastSampleBuffer: CMSampleBuffer?
  private var customCameraTexture: CustomCameraTexture?

  private var lastSampleBuffer: CMSampleBuffer? {
    get {
      var result: CMSampleBuffer?
      bufferQueue.sync {
        result = _lastSampleBuffer
      }
      return result
    }
    set {
      bufferQueue.sync {
        _lastSampleBuffer = newValue
      }
    }
  }

  init(cameraPreviewLayer: AVCaptureVideoPreviewLayer, registry: FlutterTextureRegistry) {
    self.cameraPreviewLayer = cameraPreviewLayer
    self.textureRegistry = registry
    super.init()
    self.textureId = registry.register(self)
  }

  func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
    guard let sampleBuffer = lastSampleBuffer, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
      return nil
    }

    return Unmanaged.passRetained(pixelBuffer)
  }

  func update(sampleBuffer: CMSampleBuffer) {
    lastSampleBuffer = sampleBuffer
    textureRegistry?.textureFrameAvailable(textureId!)
  }

  deinit {
    if let textureId = textureId {
      textureRegistry?.unregisterTexture(textureId)
    }
  }
}
  • The textureRegistry variable is an instance of FlutterTextureRegistry. It is used to register and unregister the Flutter texture.
  • The textureId variable stores the Flutter texture ID, which will be used to render the camera preview in Flutter.
  • The copyPixelBuffer() method returns the latest pixel buffer for texture rendering.
  • The update() method appends a new camera frame and then notifies the Flutter texture that it needs to be updated. When the textureFrameAvailable() method is invoked, the copyPixelBuffer() method is triggered to fetch the latest pixel buffer.

Create a flutterTextureEntry variable in the AppDelegate class and obtain the Flutter texture registry within the application method:

private var flutterTextureEntry: FlutterTextureRegistry?

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    ...

    guard let flutterViewController = window?.rootViewController as? FlutterViewController else {
      return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    flutterTextureEntry = flutterViewController.engine.textureRegistry

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

In the startCamera() initiate a camera session and add a video data output to capture the camera frames. When a new frame is captured in the captureOutput() method, update the CustomCameraTexture instance with the latest sample buffer:

private func startCamera(result: @escaping FlutterResult) {
    if cameraSession != nil {
      result(self.customCameraTexture?.textureId)
      return
    }

    cameraSession = AVCaptureSession()
    cameraSession?.sessionPreset = .hd1920x1080

    guard let backCamera = AVCaptureDevice.default(for: .video), let input = try? AVCaptureDeviceInput(device: backCamera) else {
      result(FlutterError(code: "no_camera", message: "No camera available", details: nil))
      return
    }

    cameraSession?.addInput(input)
    cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: cameraSession!)
    cameraPreviewLayer?.videoGravity = .resizeAspectFill

    let cameraOutput = AVCaptureVideoDataOutput()
    cameraOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
    cameraOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "camera_frame_queue"))
    cameraSession?.addOutput(cameraOutput)

    self.customCameraTexture = CustomCameraTexture(cameraPreviewLayer: cameraPreviewLayer!, registry: flutterTextureEntry!)
    cameraSession?.startRunning()

    result(self.customCameraTexture?.textureId)
  }

  func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    if connection.isVideoOrientationSupported {
      connection.videoOrientation = currentVideoOrientation()
    }
    self.customCameraTexture?.update(sampleBuffer: sampleBuffer)
  }

At this point, the camera preview should function correctly on iOS/iPadOS. Next, we will integrate the Dynamsoft Barcode Reader Bundle SDK to decode barcodes and QR codes.

Step 4: Integrating the Dynamsoft Barcode Reader Bundle SDK in Swift

The Dynamsoft Barcode Reader Bundle SDK offers a straightforward API for decoding barcodes from images. Its central entry point is the CaptureVisionRouter class, which reads barcodes from an ImageData object with a preset template. Just a few lines of code can equip your Flutter application with robust barcode scanning capabilities.

  1. Import the SDK and activate it with a valid license key in the AppDelegate.swift file:

     import DynamsoftBarcodeReaderBundle
    
     @main
     @objc class AppDelegate: FlutterAppDelegate, AVCaptureVideoDataOutputSampleBufferDelegate, LicenseVerificationListener {
    
     override func application(
         _ application: UIApplication,
         didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
       ) -> Bool {
         LicenseManager.initLicense("LICENSE-KEY", verificationDelegate: self)
    
         ...
       }
    
       func onLicenseVerified(_ isSuccess: Bool, error: Error?) {
           if isSuccess {
             print("License verification passed")
           } else {
             print("License verification failed: \(error?.localizedDescription ?? "Unknown error")")
           }
       }
     }
    
  2. Create an instance of CaptureVisionRouter in the AppDelegate:

     private let cvr = CaptureVisionRouter()
    
  3. Decode barcode and QR code from the camera frame in the captureOutput() method. Since the decoding API is CPU-intensive, to avoid blocking the camera preview rendering, we move the decoding logic to a separate thread:

     func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
         if connection.isVideoOrientationSupported {
           connection.videoOrientation = currentVideoOrientation()
         }
         self.customCameraTexture?.update(sampleBuffer: sampleBuffer)
    
         if !isProcessing {
           isProcessing = true
           DispatchQueue.global(qos: .background).async {
             self.processImage(sampleBuffer)
             self.isProcessing = false
           }
         }
       }
    

    The isProcessing boolean variable ensures that only one frame is processed at a time, and new frames are ignored until the processing is complete. This approach helps mitigate the accumulation of asynchronous tasks and prevents the app from crashing due to memory exhaustion.

  4. Implement the processImage() method to decode barcodes from CMSampleBuffer and send the results to the Flutter UI via the method channel:

     func processImage(_ sampleBuffer: CMSampleBuffer) {
         let imageBuffer:CVImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
         CVPixelBufferLockBaseAddress(imageBuffer, .readOnly)
         let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer)
         let bufferSize = CVPixelBufferGetDataSize(imageBuffer)
         let width = CVPixelBufferGetWidth(imageBuffer)
         let height = CVPixelBufferGetHeight(imageBuffer)
         let bpr = CVPixelBufferGetBytesPerRow(imageBuffer)
         CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly)
         let buffer = Data(bytes: baseAddress!, count: bufferSize)
    
         let imageData = ImageData()
         imageData.bytes = buffer
         imageData.width = UInt(width)
         imageData.height = UInt(height)
         imageData.stride = UInt(bpr)
         imageData.format = .ARGB8888
         imageData.orientation = 0
    
         let capturedResult = cvr.captureFromBuffer(imageData, templateName: PresetTemplate.readBarcodes.rawValue)
         DispatchQueue.main.async {
           self.channel?.invokeMethod("onBarcodeDetected", arguments: self.wrapResults(capturedResult: capturedResult))
         }
       }
    

    The pixel buffer is wrapped in an ImageData object with its width, height, stride, and ImagePixelFormat.ARGB8888 format. The CaptureVisionRouter.captureFromBuffer(_:templateName:) method accepts the wrapped image and the preset template PresetTemplate.readBarcodes, and returns a CapturedResult.

  5. Implement the wrapResults() method to filter the decoded barcode items and convert them into a dictionary array that can cross the method channel boundary:

     func wrapResults(capturedResult: CapturedResult?) -> NSArray {
         let outResults = NSMutableArray(capacity: 8)
         guard let items = capturedResult?.items else {
             return outResults
         }
         for item in items {
             if item.type != CapturedResultItemType.barcode {
                 continue
             }
             let barcodeItem = item as! BarcodeResultItem
             let subDic = NSMutableDictionary(capacity: 11)
             subDic.setObject(barcodeItem.formatString, forKey: "format" as NSCopying)
             let points = barcodeItem.location.points
             subDic.setObject(Int(points[0].cgPointValue.x), forKey: "x1" as NSCopying)
             subDic.setObject(Int(points[0].cgPointValue.y), forKey: "y1" as NSCopying)
             subDic.setObject(Int(points[1].cgPointValue.x), forKey: "x2" as NSCopying)
             subDic.setObject(Int(points[1].cgPointValue.y), forKey: "y2" as NSCopying)
             subDic.setObject(Int(points[2].cgPointValue.x), forKey: "x3" as NSCopying)
             subDic.setObject(Int(points[2].cgPointValue.y), forKey: "y3" as NSCopying)
             subDic.setObject(Int(points[3].cgPointValue.x), forKey: "x4" as NSCopying)
             subDic.setObject(Int(points[3].cgPointValue.y), forKey: "y4" as NSCopying)
             subDic.setObject(barcodeItem.angle, forKey: "angle" as NSCopying)
             subDic.setObject(barcodeItem.bytes, forKey: "barcodeBytes" as NSCopying)
             outResults.add(subDic)
         }
    
         return outResults
     }
    

    Note: Because the router can also return other result types, wrapResults() filters the items by CapturedResultItemType.barcode and casts each one to BarcodeResultItem before extracting the format string, location points, angle, and raw bytes. On iOS, the four location points are delivered as NSValue wrappers, so each one is unpacked with cgPointValue.

The complete AppDelegate.swift file:

import UIKit
import Flutter
import DynamsoftBarcodeReaderBundle
import AVFoundation

class CustomCameraTexture: NSObject, FlutterTexture {
  private weak var textureRegistry: FlutterTextureRegistry?
  var textureId: Int64?
  private var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
  private let bufferQueue = DispatchQueue(label: "com.example.flutter/barcode_scan")
  private var _lastSampleBuffer: CMSampleBuffer?
  private var customCameraTexture: CustomCameraTexture?

  private var lastSampleBuffer: CMSampleBuffer? {
    get {
      var result: CMSampleBuffer?
      bufferQueue.sync {
        result = _lastSampleBuffer
      }
      return result
    }
    set {
      bufferQueue.sync {
        _lastSampleBuffer = newValue
      }
    }
  }

  init(cameraPreviewLayer: AVCaptureVideoPreviewLayer, registry: FlutterTextureRegistry) {
    self.cameraPreviewLayer = cameraPreviewLayer
    self.textureRegistry = registry
    super.init()
    self.textureId = registry.register(self)
  }

  func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
    guard let sampleBuffer = lastSampleBuffer, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
      return nil
    }

    return Unmanaged.passRetained(pixelBuffer)
  }

  func update(sampleBuffer: CMSampleBuffer) {
    lastSampleBuffer = sampleBuffer
    textureRegistry?.textureFrameAvailable(textureId!)
  }

  deinit {
    if let textureId = textureId {
      textureRegistry?.unregisterTexture(textureId)
    }
  }
}

@main
@objc class AppDelegate: FlutterAppDelegate, AVCaptureVideoDataOutputSampleBufferDelegate, LicenseVerificationListener {

  private var flutterTextureEntry: FlutterTextureRegistry?
  private var cameraSession: AVCaptureSession?
  private var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
  private var customCameraTexture: CustomCameraTexture?
  private let CHANNEL = "barcode_scan"
  private var width = 1920
  private var height = 1080
  private var isProcessing = false
  private var channel: FlutterMethodChannel?
  private let cvr = CaptureVisionRouter()

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    LicenseManager.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==", verificationDelegate: self)

    GeneratedPluginRegistrant.register(with: self)

    guard let flutterViewController = window?.rootViewController as? FlutterViewController else {
      return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    flutterTextureEntry = flutterViewController.engine.textureRegistry

    channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: flutterViewController.binaryMessenger)
    channel?.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if call.method == "startCamera" {
        self.startCamera(result: result)
      } else if call.method == "getPreviewWidth" {
        result(self.width)
      } else if call.method == "getPreviewHeight" {
        result(self.height)
      }

      else {
        result(FlutterMethodNotImplemented)
      }
    })

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  func onLicenseVerified(_ isSuccess: Bool, error: Error?) {
    if isSuccess {
      print("License verification passed")
    } else {
      print("License verification failed: \(error?.localizedDescription ?? "Unknown error")")
    }
  }

  private func startCamera(result: @escaping FlutterResult) {
    if cameraSession != nil {
      result(self.customCameraTexture?.textureId)
      return
    }

    cameraSession = AVCaptureSession()
    cameraSession?.sessionPreset = .hd1920x1080

    guard let backCamera = AVCaptureDevice.default(for: .video), let input = try? AVCaptureDeviceInput(device: backCamera) else {
      result(FlutterError(code: "no_camera", message: "No camera available", details: nil))
      return
    }

    cameraSession?.addInput(input)
    cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: cameraSession!)
    cameraPreviewLayer?.videoGravity = .resizeAspectFill

    let cameraOutput = AVCaptureVideoDataOutput()
    cameraOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
    cameraOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "camera_frame_queue"))
    cameraSession?.addOutput(cameraOutput)

    self.customCameraTexture = CustomCameraTexture(cameraPreviewLayer: cameraPreviewLayer!, registry: flutterTextureEntry!)
    cameraSession?.startRunning()

    result(self.customCameraTexture?.textureId)
  }

  func currentVideoOrientation() -> AVCaptureVideoOrientation {
    switch UIDevice.current.orientation {
    case .portrait:
      return .portrait
    case .portraitUpsideDown:
      return .portraitUpsideDown
    case .landscapeLeft:
      return .landscapeRight
    case .landscapeRight:
      return .landscapeLeft
    default:
      return .portrait
    }
  }

  func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    if connection.isVideoOrientationSupported {
      connection.videoOrientation = currentVideoOrientation()
    }
    self.customCameraTexture?.update(sampleBuffer: sampleBuffer)

    if !isProcessing {
      isProcessing = true
      DispatchQueue.global(qos: .background).async {
        self.processImage(sampleBuffer)
        self.isProcessing = false
      }
    }
  }

  func processImage(_ sampleBuffer: CMSampleBuffer) {
    let imageBuffer:CVImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
    CVPixelBufferLockBaseAddress(imageBuffer, .readOnly)
    let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer)
    let bufferSize = CVPixelBufferGetDataSize(imageBuffer)
    let width = CVPixelBufferGetWidth(imageBuffer)
    let height = CVPixelBufferGetHeight(imageBuffer)
    let bpr = CVPixelBufferGetBytesPerRow(imageBuffer)
    CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly)
    let buffer = Data(bytes: baseAddress!, count: bufferSize)

    let imageData = ImageData()
    imageData.bytes = buffer
    imageData.width = UInt(width)
    imageData.height = UInt(height)
    imageData.stride = UInt(bpr)
    imageData.format = .ARGB8888
    imageData.orientation = 0

    let capturedResult = cvr.captureFromBuffer(imageData, templateName: PresetTemplate.readBarcodes.rawValue)
    DispatchQueue.main.async {
      self.channel?.invokeMethod("onBarcodeDetected", arguments: self.wrapResults(capturedResult: capturedResult))
    }
  }

  func wrapResults(capturedResult: CapturedResult?) -> NSArray {
    let outResults = NSMutableArray(capacity: 8)
    guard let items = capturedResult?.items else {
      return outResults
    }
    for item in items {
      if item.type != CapturedResultItemType.barcode {
        continue
      }
      let barcodeItem = item as! BarcodeResultItem
      let subDic = NSMutableDictionary(capacity: 11)
      subDic.setObject(barcodeItem.formatString, forKey: "format" as NSCopying)
      let points = barcodeItem.location.points
      subDic.setObject(Int(points[0].cgPointValue.x), forKey: "x1" as NSCopying)
      subDic.setObject(Int(points[0].cgPointValue.y), forKey: "y1" as NSCopying)
      subDic.setObject(Int(points[1].cgPointValue.x), forKey: "x2" as NSCopying)
      subDic.setObject(Int(points[1].cgPointValue.y), forKey: "y2" as NSCopying)
      subDic.setObject(Int(points[2].cgPointValue.x), forKey: "x3" as NSCopying)
      subDic.setObject(Int(points[2].cgPointValue.y), forKey: "y3" as NSCopying)
      subDic.setObject(Int(points[3].cgPointValue.x), forKey: "x4" as NSCopying)
      subDic.setObject(Int(points[3].cgPointValue.y), forKey: "y4" as NSCopying)
      subDic.setObject(barcodeItem.angle, forKey: "angle" as NSCopying)
      subDic.setObject(barcodeItem.bytes, forKey: "barcodeBytes" as NSCopying)
      outResults.add(subDic)
    }

    return outResults
  }
}

Running the Flutter QR Code Scanner on iOS

flutter run

Flutter iOS QR code scanner

Common Issues & Edge Cases

  • “DynamsoftBarcodeReaderBundle requires a higher minimum deployment target” when running pod install. The bundle requires iOS 13.0 or newer. Declare platform :ios, '13.0' in the Podfile and set IPHONEOS_DEPLOYMENT_TARGET to 13.0 in the Xcode project.
  • License verification is asynchronous. LicenseManager.initLicense(_:verificationDelegate:) returns immediately and verifies the license on a background thread, so frames decoded before verification completes will fail. Allow a moment after the app launches before testing barcode scanning.
  • Camera permission denied on first launch. The permission dialog appears the first time the app starts. If the user denies it, the preview stays blank until the permission is granted in system settings and the app is restarted.
  • Pixel format mismatch. The decode expects the pixel buffer format to match the ImageData format passed to captureFromBuffer. The sample captures frames in kCVPixelFormatType_32BGRA and declares ImagePixelFormat.ARGB8888, which the barcode engine maps internally. If you change videoSettings, update the ImageData format and stride accordingly.
  • Only one frame is processed at a time. The isProcessing flag drops the incoming frames while a decode is in progress on the background queue. If you need a higher frame rate, move the flag assignment inside processImage and keep captureOutput lightweight.

Source Code

https://github.com/yushulx/flutter-barcode-mrz-document-scanner/tree/main/examples/native_camera