Previously, I wrote a blog post sharing how to implement a Flutter QR code scanner plugin for Android. For cross-platform mobile development, a plugin is not perfect if it does not support both Android and iOS. In this article, I am going to complement the iOS part of the plugin in order to bring the best development experience for mobile developers.
Flutter QR Code Scanner Plugin
https://pub.dev/packages/flutter_camera_qrcode_scanner
Dev Environment
- M1 Mac
- Xcode 13.2.1
Step-by-step Guide: Implement Flutter QR Code Scanner Plugin for iOS
Step 1: Get the Existing Flutter Plugin Project:
git clone https://github.com/yushulx/flutter_qrcode_scanner
Step 2: Add Support for iOS
cd flutter_qrcode_scanner
flutter create --template=plugin --platforms=ios .
Step 3: Install Dynamsoft Camera Enhancer and Dynamsoft Barcode Reader
Open iOS/flutter_camera_qrcode_scanner.podspec
to add Dynamsoft Camera Enhancer 2.1.1 and Dynamsoft Barcode Reader 8.9.1:
s.dependency 'DynamsoftBarcodeReader', '8.9.1'
s.dependency 'DynamsoftCameraEnhancer', '2.1.1'
The dependencies will be installed via pod install
.
Activating Mobile QR Code SDK
Click here to get a valid license key for Dynamsoft Barcode Reader.
Step 4: Implement the Factory and the Platform View Using Swift Code
Since we have completed the code logic on the Dart side, we only need to focus on the platform-related code. According to the official tutorial and the Android part of the plugin, we can write Swift code for iOS as follows:
-
Based on the structure of the Android QRCodeScanner class, we create a
FLQRCodeScanner
class iniOS/Classes/FLQRCodeScanner.swift
:
import Flutter import UIKit import DynamsoftBarcodeReader import DynamsoftCameraEnhancer public protocol DetectionHandler { func onDetected(data: NSArray) } class FLQRCodeScanner: NSObject, DBRTextResultDelegate { private var cameraView: DCECameraView private var dce: DynamsoftCameraEnhancer private var barcodeReader: DynamsoftBarcodeReader! = nil private var handler: DetectionHandler? init(cameraView: DCECameraView, dce: DynamsoftCameraEnhancer) { self.cameraView = cameraView self.cameraView.overlayVisible = true self.dce = dce super.init() createBarcodeReader(dce: dce) } func setDetectionHandler(handler: DetectionHandler) { self.handler = handler; } func createBarcodeReader(dce: DynamsoftCameraEnhancer) { // To activate the sdk, apply for a license key: https://www.dynamsoft.com/customer/license/trialLicense?product=dbr barcodeReader = DynamsoftBarcodeReader.init(license: "license-key") barcodeReader.setCameraEnhancer(dce) // Set text result call back to get barcode results. barcodeReader.setDBRTextResultDelegate(self, userData: nil) // Start the barcode decoding thread. barcodeReader.startScanning() } func textResultCallback(_ frameId: Int, results: [iTextResult]?, userData: NSObject?) { if results!.count > 0 { let outResults = NSMutableArray() for item in results! { let subDic = NSMutableDictionary() if item.barcodeFormat_2 != EnumBarcodeFormat2.Null { subDic.setObject(item.barcodeFormatString_2 ?? "", forKey: "format" as NSCopying) }else{ subDic.setObject(item.barcodeFormatString ?? "", forKey: "format" as NSCopying) } subDic.setObject(item.barcodeText ?? "", forKey: "text" as NSCopying) let points = item.localizationResult?.resultPoints as! [CGPoint] subDic.setObject(Int(points[0].x), forKey: "x1" as NSCopying) subDic.setObject(Int(points[0].y), forKey: "y1" as NSCopying) subDic.setObject(Int(points[1].x), forKey: "x2" as NSCopying) subDic.setObject(Int(points[1].y), forKey: "y2" as NSCopying) subDic.setObject(Int(points[2].x), forKey: "x3" as NSCopying) subDic.setObject(Int(points[2].y), forKey: "y3" as NSCopying) subDic.setObject(Int(points[3].x), forKey: "x4" as NSCopying) subDic.setObject(Int(points[3].y), forKey: "y4" as NSCopying) subDic.setObject(item.localizationResult?.angle ?? 0, forKey: "angle" as NSCopying) outResults.add(subDic) } if handler != nil { handler!.onDetected(data: outResults) } } } func startScan() { cameraView.overlayVisible = true barcodeReader.startScanning() } func stopScan() { cameraView.overlayVisible = false barcodeReader.stopScanning() } func setBarcodeFormats(arg:NSDictionary) { let formats:Int = arg.value(forKey: "formats") as! Int let settings = try! barcodeReader!.getRuntimeSettings() settings.barcodeFormatIds = formats barcodeReader!.update(settings, error: nil) } func setLicense(license: String) { barcodeReader.license = license } }
-
Then we create a
FLNativeView
class iniOS/Classes/FLNativeView.swift
to implement the camera view:
import Flutter import UIKit import DynamsoftCameraEnhancer class FLNativeView: NSObject, FlutterPlatformView, DetectionHandler { private var _view: UIView private var messenger: FlutterBinaryMessenger private var channel: FlutterMethodChannel private var qrCodeScanner: FLQRCodeScanner init( frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, binaryMessenger: FlutterBinaryMessenger ) { self.messenger = binaryMessenger let cameraView = DCECameraView.init(frame: frame) let dce = DynamsoftCameraEnhancer.init(view: cameraView) dce.open() dce.setFrameRate(30) _view = cameraView qrCodeScanner = FLQRCodeScanner.init(cameraView: cameraView, dce: dce) channel = FlutterMethodChannel(name: "com.dynamsoft.flutter_camera_qrcode_scanner/nativeview_" + String(viewId), binaryMessenger: messenger) super.init() qrCodeScanner.setDetectionHandler(handler: self) channel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch call.method { case "startScanning": self.qrCodeScanner.startScan() result(.none) case "stopScanning": self.qrCodeScanner.stopScan() result(.none) case "setLicense": self.qrCodeScanner.setLicense(license: (call.arguments as! NSDictionary).value(forKey: "license") as! String) result(.none) case "setBarcodeFormats": self.qrCodeScanner.setBarcodeFormats(arg: call.arguments as! NSDictionary) result(.none) default: result(.none) } }) } func view() -> UIView { return _view } func onDetected(data: NSArray) { DispatchQueue.main.async { self.channel.invokeMethod("onDetected", arguments: data) } } }
The native view contains a channel to handle Flutter method calls (from Dart to native). It also triggers the
onDetected
callback function (from native to Dart). When a QR code is detected, the channel will send the QR information from the native side to the Dart side . -
Next, create a
FLNativeViewFactory
class iniOS/Classes/FLNativeViewFactory.swift
to initialize the native view:
import Flutter import UIKit class FLNativeViewFactory: NSObject, FlutterPlatformViewFactory { private var messenger: FlutterBinaryMessenger init(messenger: FlutterBinaryMessenger) { self.messenger = messenger super.init() } func create( withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any? ) -> FlutterPlatformView { return FLNativeView( frame: frame, viewIdentifier: viewId, arguments: args, binaryMessenger: messenger ) } }
-
The final step is to register the factory in
iOS/Classes/SwiftFlutterCameraQrcodeScannerPlugin.swift
:
import Flutter import UIKit public class SwiftFlutterCameraQrcodeScannerPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let factory = FLNativeViewFactory(messenger: registrar.messenger()) registrar.register(factory, withId: "com.dynamsoft.flutter_camera_qrcode_scanner/nativeview") } }
So far, the native Swift code for plugin is done.
Step 5: Test the QR code scanner in Flutter
-
Go to the example folder and add the camera access permission to
ios/Runner/Info.plist
:
<key>NSCameraUsageDescription</key> <string>Can I use the camera please?</string> <key>NSMicrophoneUsageDescription</key> <string>Can I use the mic please?</string>
-
Run the example on iOS devices:
flutter run