How to Implement a Flutter QR Code Scanner Plugin for iOS in Swift

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.3 and Dynamsoft Barcode Reader 9.0.1:

s.dependency 'DynamsoftBarcodeReader', '9.0.1'
s.dependency 'DynamsoftCameraEnhancer', '2.1.3'

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:

  1. Based on the structure of the Android QRCodeScanner class, we create a FLQRCodeScanner class in iOS/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?
    
         func initScanner(cameraView: DCECameraView, dce: DynamsoftCameraEnhancer) {
             self.cameraView = cameraView
             self.cameraView.overlayVisible = true
             self.dce = dce
        
             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&source=codepool
             barcodeReader = DynamsoftBarcodeReader()
             barcodeReader.setCameraEnhancer(dce)
        
             // Set text result call back to get barcode results.
             barcodeReader.setDBRTextResultListener(self)
         }
    
         func textResultCallback(_ frameId: Int, imageData: iImageData, results: [iTextResult]?) {
             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() {
             dce.open();
             cameraView.overlayVisible = true
             barcodeReader.startScanning()
         }
        
         func stopScan() {
             dce.close();
             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, verificationDelegate: DBRLicenseVerificationListener) {
             DynamsoftBarcodeReader.initLicense(license, verificationDelegate: verificationDelegate)    
         }
     }
    
  2. Then we create a FLNativeView class in iOS/Classes/FLNativeView.swift to implement the camera view:

     import Flutter
     import UIKit
     import DynamsoftCameraEnhancer
        
     class FLNativeView: NSObject, FlutterPlatformView, DetectionHandler, DBRLicenseVerificationListener {
         private var _view: UIView
         private var messenger: FlutterBinaryMessenger
         private var channel: FlutterMethodChannel
         private var qrCodeScanner: FLQRCodeScanner
         var completionHandlers: [FlutterResult] = []
         init(
             frame: CGRect,
             viewIdentifier viewId: Int64,
             arguments args: Any?,
             binaryMessenger: FlutterBinaryMessenger
         ) {
             self.messenger = binaryMessenger
             // The default frame CGRect is (0, 0, 0, 0).
             if (frame.width == 0 || frame.height == 0) {
                 cameraView = DCECameraView.init(frame: UIScreen.main.bounds)
             }
             else {
                 cameraView = DCECameraView.init(frame: frame)
             }
    
             let dce = DynamsoftCameraEnhancer.init(view: cameraView)
             _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 "init":
                         self.qrCodeScanner.initScanner(cameraView: self.cameraView, dce: self.dce)
                         result(.none)
                     case "startScanning":
                         self.qrCodeScanner.startScan()
                         result(.none)
                     case "stopScanning":
                         self.qrCodeScanner.stopScan()
                         result(.none)
                     case "setLicense":
                         self.completionHandlers.append(result)
                     self.qrCodeScanner.setLicense(license: (call.arguments as! NSDictionary).value(forKey: "license") as! String, verificationDelegate: self)        
                     case "setBarcodeFormats":
                         self.qrCodeScanner.setBarcodeFormats(arg: call.arguments as! NSDictionary)
                         result(.none)
                     default:
                         result(.none)
                     }
             })
         }
            
         public func dbrLicenseVerificationCallback(_ isSuccess: Bool, error: Error?)
         {
             completionHandlers.first?(.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 .

  3. Next, create a FLNativeViewFactory class in iOS/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
             )
         }
     }
    
  4. 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

  1. 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>
    
  2. Run the example on iOS devices:

     flutter run
    

Flutter iOS QR code scanner

Source Code

https://github.com/yushulx/flutter_qrcode_scanner