Build a React Native QR Code Scanner Native UI Component

React Native is an open-source UI software framework made by Facebook. It enables developers to use the React framework to create applications for Android, iOS, macOS, Windows, etc.

The architecture of React Native is illustrated in the following diagram 1:

React Native architecture

As you can see, there are four core sections. The React code we write is interpreted by the JavaScript engine. The engine communicates with the native via a bridge. The bridge uses asynchronous JSON messages for communication.

React Native uses the NativeModule and the NativeComponent systems to provide access to native platform APIs and UI widgets. NativeComponent is similar to NativeModule. The major difference is that we can use NativeComponent as a view in our React code2.

In the previous articles, we have talked about different ways to integrate Dynamsoft Barcode Reader (DBR) in a React Native app.

In this article, we are going to create a React Native barcode/QR Code scanner Native UI component using Dynamsoft Barcode Reader. Since there is Dynamsoft Camera Enhancer (DCE) which makes the use of the camera on Android and iOS easy, we are going to use DCE with DBR to build the component.

A demo app running on iOS:

iOS Screenshot

Create a QR code Scanner Native UI Component

Environment

  1. Node
  2. Android Studio
  3. Xcode

Init a New Native UI Component Library Project

Run the following command:

npx create-react-native-library react-native-dynamsoft-barcode-scanner

It will ask you to enter descriptions of the project, select which languages to use and specify the type of the library.

Here we choose Java+Swift to create a component project.

√ Which languages do you want to use? » Java & Swift
√ What type of library do you want to develop? » Native view (to use as a component)

There is an example folder in the project. We can use it to test the library.

We can navigate into the project folder and run the following command to start the example.

# bootstrap the example project
yarn
# Android app
yarn example android
# iOS app
yarn example ios

iOS Implementation

Let’s implement the library for iOS first.

Overview of the iOS Library

In the ios folder, there are three code files:

DynamsoftBarcodeScanner-Bridging-Header.h
DynamsoftBarcodeScannerViewManager.m
DynamsoftBarcodeScannerViewManager.swift

In the swift file, there is a view manager which returns a custom view. The custom view has a property called color.

@objc(DynamsoftBarcodeScannerViewManager)
class DynamsoftBarcodeScannerViewManager: RCTViewManager {

  override func view() -> (DynamsoftBarcodeScannerView) {
    return DynamsoftBarcodeScannerView()
  }
}

class DynamsoftBarcodeScannerView : UIView {

  @objc var color: String = "" {
    didSet {
      self.backgroundColor = hexStringToUIColor(hexColor: color)
    }
  }
}

In the .m file, the color property is exported.

#import "React/RCTViewManager.h"

@interface RCT_EXTERN_MODULE(DynamsoftBarcodeScannerViewManager, RCTViewManager)

RCT_EXPORT_VIEW_PROPERTY(color, NSString)

@end

Let’s open the index.js file to learn about how the custom view is defined as a component in React.

import {
  requireNativeComponent,
  UIManager,
  Platform,
  ViewStyle,
} from 'react-native';

const LINKING_ERROR =
  `The package 'react-native-dynamsoft-barcode-scanner' doesn't seem to be linked. Make sure: \n\n` +
  Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
  '- You rebuilt the app after installing the package\n' +
  '- You are not using Expo managed workflow\n';

type DynamsoftBarcodeScannerProps = {
  color: string;
  style: ViewStyle;
};

const ComponentName = 'DynamsoftBarcodeScannerView';

export const DynamsoftBarcodeScannerView =
  UIManager.getViewManagerConfig(ComponentName) != null
    ? requireNativeComponent<DynamsoftBarcodeScannerProps>(ComponentName)
    : () => {
        throw new Error(LINKING_ERROR);
      };

To use the component in the example/src/App.tsx file:

import { DynamsoftBarcodeScannerView } from 'react-native-dynamsoft-barcode-scanner';
<DynamsoftBarcodeScannerViewManager color="#32a852" style={styles.box} />

Import Frameworks

We need to import the frameworks of DBR and DCE first

  1. Open react-native-dynamsoft-barcode-scanner.podspec and add the following lines:

     s.libraries = 'c++'
     s.vendored_frameworks = 'DynamsoftBarcodeReader.framework', 'DynamsoftCameraEnhancer.framework'
    
  2. Download the frameworks and put them under the project’s folder. Download links: DBR, DCE

  3. Update pods

     pod install
    

In addition, licenses are needed to use Dynamsoft Barcode Reader and Dynamsoft Camera Enhancer. You can apply for a trial license here.

Integrate DCE

Let’s open the swift file and do the following:

  1. Import DCE

     import DynamsoftCameraEnhancer
    
  2. Add properties

     var dce:DynamsoftCameraEnhancer! = nil
     var dceView:DCECameraView! = nil
    
  3. Config DCE. Add the DCE CameraView to the root UIView.

     func configurationDCE() {
         // Initialize a camera view for previewing video.
         dceView = DCECameraView.init(frame: self.bounds)
         self.addSubview(dceView)
         dceView.overlayVisible = true
         dce = DynamsoftCameraEnhancer.init(view: dceView)
     }
    

We also need to add the following to the Info.plist for camera permission:

<key>NSCameraUsageDescription</key>
<string>For barcode scanning</string>
<key>NSMicrophoneUsageDescription</key>
<string>For barcode scanning</string>

Start the Camera

After DCE is integrated, we can use it to call the camera.

  1. Change the view’s name to Scanner in the index.tsx

     - export const DynamsoftBarcodeScannerView =
     + export const Scanner =
    
  2. Add a isScanning property to control the scanning status

    In the index.tsx:

     type DynamsoftBarcodeScannerProps = {
     -  color: string;
     +  isScanning: isScanning;
        style: ViewStyle;
     };
    
  3. Update the component in the example/src/App.tsx file

     import { Scanner } from 'react-native-dynamsoft-barcode-scanner';
     <Scanner isScanning={true} style={styles.scanner} />
    
  4. In the DynamsoftBarcodeScannerViewManager.swift file, add the isScanning property

     @objc var isScanning: Bool = false {
         didSet {
             if isScanning
             {
                 if (dce == nil){
                     configurationDCE()
                     dce.open()
                 }else{
                     dce.resume()
                 }
             }else{
                 if dce != nil {
                     dce.pause()
                 }
             }
         }
     }
    
  5. In the DynamsoftBarcodeScannerViewManager.m file, export the isScanning property

     RCT_EXPORT_VIEW_PROPERTY(isScanning, BOOL)
    
  6. We can add a button to control the isScanning property

     const [isScanning, setIsScanning] = useState(false);
     const [btnText, setBtnText] = useState('Start Scan');
     const toggleScan = () =>  {
       if (isScanning == true){
         setIsScanning(false);
         setBtnText("Start Scan");
       }else{
         setIsScanning(true);
         setBtnText("Stop Scan");
       }
     }
     <View style=>
       <TouchableOpacity onPress={toggleScan} >
         <Text style=> {btnText} </Text>
       </TouchableOpacity>
     </View>
    

Okay. We can now use Xcode to open the example project to have a test.

Bind DBR with DCE to Read Barcodes

Now that we can show the camera preview, we can integrate DBR to read barcodes.

  1. Import DBR

     import DynamsoftBarcodeReader
    
  2. Add properties

     var barcodeReader:DynamsoftBarcodeReader! = nil
     @objc var dbrLicense: String = "<license>"
    
  3. Config DBR

     func configurationDBR() {
         barcodeReader = DynamsoftBarcodeReader(license: dbrLicense)
     }
    
  4. Bind DCE and DBR

     func bindDCEtoDBR(){
         let para = iDCESettingParameters.init()
         para.cameraInstance = dce
         para.textResultDelegate = self
         barcodeReader.setCameraEnhancerPara(para)
     }
        
     public func textResultCallback(_ frameId: Int, results: [iTextResult]?, userData: NSObject?) {
         let count = results?.count ?? 0
         if count > 0 {
             print("Found barcodes")
         }
     }
    

We can get the barcodes info in the textResultCallback.

Send Scanned Event from Native to JavaScript

The RCTViewManager has a bridge property. We can use it to send scanned barcodes to JavaScript.

Here is the code to use it:

var bridge:RCTBridge! = nil
public func textResultCallback(_ frameId: Int, results: [iTextResult]?, userData: NSObject?) {
    let count = results?.count ?? 0
    let array = NSMutableArray()
    for index in 0..<count {
        let tr = results![index]
        let result = NSMutableDictionary()
        result["barcodeText"] = tr.barcodeText
        result["barcodeFormat"] = tr.barcodeFormatString
        result["barcodeBytesBase64"] = tr.barcodeBytes?.base64EncodedString()
        array.add(result)
    }
    bridge.eventDispatcher().sendDeviceEvent(withName: "onScanned", body: array)
}

We can then receive the event on the JavaScript side and display the barcodes info:

const [barcodesInfo, setBarcodesInfo] = useState('');
const onScanned = (results:Array<ScanResult>) => {
  var info = "";
  for (var i=0;i<results.length;i++){
    let result = results[i];
    info = info + result.barcodeFormat + ": " + result.barcodeText + "\n";
  }
  console.log(info)
}
DeviceEventEmitter.addListener('onScanned',onScanned);
return (
  <View style={styles.container}>
    <View style=>
      <Text style=> {barcodesInfo} </Text>
    </View>
    <Scanner 
      isScanning={isScanning}
      style={styles.scanner}
      onScanned={onScanned}
    />
  </View>
);

We also need to modify the index.tsx file to update the props of the component and define a ScanResult interface.

type DynamsoftBarcodeScannerProps = {
  isScanning: boolean;
  style: ViewStyle;
  onScanned?: Event;
};

export interface ScanResult{
  barcodeText: string;
  barcodeFormat: string;
  barcodeBytesBase64: string;
}

All right, the scanner’s basic iOS implementation is done.

Add More Functions

We can add more functions to the library like flashlight control and camera switcher. We can also modify Dynamsoft Barcode Reader’s runtime settings to decode QR codes only.

Let’s take updating the runtime settings for example.

  1. Add a new template property in index.tsx

     type DynamsoftBarcodeScannerProps = {
       template?: string;
     };
    
  2. Add a new template property in example/src/App.tsx to use a JSON template specifying the QR code barcode format

     const template = "{\"ImageParameter\":{\"BarcodeFormatIds\":[\"BF_QR_CODE\"],\"Description\":\"\",\"Name\":\"Settings\"},\"Version\":\"3.0\"}";
     //......
      <Scanner 
       isScanning={isScanning} 
       style={styles.scanner} 
       onScanned={onScanned}
       template={template}
     />
    
  3. Update the runtime settings in DynamsoftBarcodeScannerViewManager.swift

     @objc var template: String = "" {
         didSet {
             updateTemplate()
         }
     }
        
     func updateTemplate(){
         if barcodeReader != nil {
             if (template != ""){
                 var error: NSError? = NSError()
                 barcodeReader.initRuntimeSettings(with: template, conflictMode: EnumConflictMode.overwrite, error: &error)
             }else{
                 var error: NSError? = NSError()
                 barcodeReader.resetRuntimeSettings(&error)
             }
         }
     }
    

Okay. The barcode reader has been set to decode QR codes only.

Android Implementation

The Android implementation is much the same. Here are the things worth mentioning.

  1. The Android libraries of DBR and DCE can be imported by adding the following to the build.gradle file:

     rootProject.allprojects {
         repositories {
             maven {
                 url "http://download2.dynamsoft.com/maven/dbr/aar"
             }
             maven {
                 url "https://download2.dynamsoft.com/maven/dce/aar"
             }
         }
     }
    
     dependencies {
         //noinspection GradleDynamicVersion
         implementation "com.facebook.react:react-native:+"  // From node_modules
         implementation 'com.dynamsoft:dynamsoftcameraenhancer:2.0.0@aar'
         implementation 'com.dynamsoft:dynamsoftbarcodereader:8.8.0@aar'
     }
    
  2. There is no need to manage the camera permission since DCE Android does this for us

  3. The view manager can directly return the DCE camera view instead of creating a new view class

     @Override
     @NonNull
     public DCECameraView createViewInstance(ThemedReactContext reactContext) {
         context = reactContext;
         reactContext.addLifecycleEventListener(this);
         mCameraView = new DCECameraView(reactContext.getBaseContext());
         return mCameraView;
     }
    
  4. We need to manage the life cycle for an Android app

     @Override
     public void onHostResume() {
         if (reader!=null){
             if (this.isScanning){
                 reader.StartCameraEnhancer();
             }
         }
     }
    
     @Override
     public void onHostPause() {
         if (reader!=null){
             reader.StopCameraEnhancer();
         }
     }
    
     @Override
     public void onHostDestroy() {
         if (reader!=null){
             reader.destroy();
             reader=null;
             mCameraEnhancer=null;
         }
         context.removeLifecycleEventListener(this);
     }
    
  5. The RCTDeviceEventEmitter is used to send events from Native to JavaScript

     context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onScanned",results);
    

Source Code

Here is the complete source code:

https://github.com/xulihang/react-native-dynamsoft-barcode-scanner

You can add the library to your React Native project following its instruction.

References