How to Implement a Flutter QR Code Scanner Plugin for iOS in Swift
Tags: flutter, ios, swift, qrcode
Published: 2022-03-11 09:11:27

Previously, I wrote a [blog post](https://www.dynamsoft.com/codepool/flutter-qr-code-scanner.html) 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](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:

```bash
git clone https://github.com/yushulx/flutter_qrcode_scanner
```

### Step 2: Add Support for iOS

```bash
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](https://www.dynamsoft.com/camera-enhancer/docs/introduction/) 2.1.1 and [Dynamsoft Barcode Reader](https://www.dynamsoft.com/barcode-reader/overview/) 8.9.1:

```ruby
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](https://www.dynamsoft.com/customer/license/trialLicense/?product=dbr) 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](https://docs.flutter.dev/development/platform-integration/platform-views?tab=ios-platform-views-swift-tab) 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](https://github.com/yushulx/flutter_qrcode_scanner/blob/main/android/src/main/java/com/dynamsoft/flutter_camera_qrcode_scanner/QRCodeScanner.java) class, we create a `FLQRCodeScanner` class in `iOS/Classes/FLQRCodeScanner.swift`:
    
    ```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
        }
    }
    ```

2. Then we create a `FLNativeView` class in `iOS/Classes/FLNativeView.swift` to implement the camera view:
    
    ```swift
    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 .

3. Next, create a `FLNativeViewFactory` class in `iOS/Classes/FLNativeViewFactory.swift` to initialize the native view:

    ```swift
    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`:
    ```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`:

    ```xml
    <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:
    
    ```bash
    flutter run
    ```

    ![Flutter iOS QR code scanner](https://www.dynamsoft.com/codepool/img/2022/03/flutter-ios-qr-code-scanner.png)

## Source Code
[https://github.com/yushulx/flutter_qrcode_scanner](https://github.com/yushulx/flutter_qrcode_scanner)