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.
This article is Part 2 in a 2-Part Series.
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
Textureand displaying it in Dart with aTexturewidget avoids the per-frame memory copies between native code and Dart that make camera plugins slow for image processing. - AVFoundation
AVCaptureVideoDataOutputframes are decoded on a background thread by wrapping the pixel buffer in anImageDataobject and callingCaptureVisionRouter.captureFromBuffer(_:templateName:)with thePresetTemplate.readBarcodestemplate. - The legacy
DynamsoftBarcodeReader9.x pod and itsBarcodeReader.decodeBuffer()/iImageDataAPI have been superseded by theDynamsoftBarcodeReaderBundlepod (v11.x), which contains the same barcode engine as Dynamsoft Capture Vision and is initialized withLicenseManager.initLicense(). - Barcode results cross the Swift–Dart boundary through a
MethodChannelas a list of dictionaries (format, coordinates,angle,barcodeBytes), and the Dart side draws the overlay while the native side filters the decoded items byCapturedResultItemType.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
- Get a 30-day free trial license
- Flutter SDK with the iOS toolchain, CocoaPods, and an iPhone (or iPad) for testing.
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:
- 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.
- 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
channelvariable is an instance ofFlutterMethodChannel. It receives method calls fromDartusing thesetMethodCallHandlermethod. Additionally, theinvokeMethodmethod is used to send data fromSwifttoDart. - The
widthandheightvariables, which store the camera preview size, are hardcoded to1920x1080here. The methodsgetPreviewWidthandgetPreviewHeightretrieve 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
textureRegistryvariable is an instance ofFlutterTextureRegistry. It is used to register and unregister the Flutter texture. - The
textureIdvariable 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 thetextureFrameAvailable()method is invoked, thecopyPixelBuffer()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.
-
Import the SDK and activate it with a valid license key in the
AppDelegate.swiftfile: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")") } } } -
Create an instance of
CaptureVisionRouterin theAppDelegate:private let cvr = CaptureVisionRouter() -
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
isProcessingboolean 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. -
Implement the
processImage()method to decode barcodes fromCMSampleBufferand 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
ImageDataobject with its width, height, stride, andImagePixelFormat.ARGB8888format. TheCaptureVisionRouter.captureFromBuffer(_:templateName:)method accepts the wrapped image and the preset templatePresetTemplate.readBarcodes, and returns aCapturedResult. -
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 byCapturedResultItemType.barcodeand casts each one toBarcodeResultItembefore extracting the format string, location points, angle, and raw bytes. On iOS, the four location points are delivered asNSValuewrappers, so each one is unpacked withcgPointValue.
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

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 setIPHONEOS_DEPLOYMENT_TARGETto 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
ImageDataformat passed tocaptureFromBuffer. The sample captures frames inkCVPixelFormatType_32BGRAand declaresImagePixelFormat.ARGB8888, which the barcode engine maps internally. If you changevideoSettings, update theImageDataformat and stride accordingly. - Only one frame is processed at a time. The
isProcessingflag 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 insideprocessImageand keepcaptureOutputlightweight.
Source Code
https://github.com/yushulx/flutter-barcode-mrz-document-scanner/tree/main/examples/native_camera