Composite Scanning

Composite scanning allows you to set up a workflow where multiple items need to be scanned.

A ViewPluginComposite is a special type of ScanViewPlugin that holds any number of "child" ScanViewPlugins. There are three processing modes available in a plugin composite: sequential, parallel, and parallelFirstScan:

  • In sequential mode, the plugin composite runs each child plugin sequentially in the order they were added. As each plugin gets a result, the next plugin is started.

  • In parallel mode, cutouts for each child plugin are initially displayed simultaneously. As a plugin gets a result, its cutout is hidden so that the user can concentrate on scanning the remaining items. For instance, you can set up a parallel scan configuration consisting of an electric meter together with a barcode.

As soon as the last child plugin has scanned successfully, the results for each child scan view plugin are combined and returned as a group.

As of SDK version 44.1.0, there is an additional mode available called parallelFirstScan:

  • parallelFirstScan mode operates similar to parallel mode, displaying cutouts for all child plugins initially. However, scanning will complete upon getting the first scan result it sees and returning its value.

Nesting plugin composites is currently not supported. In other words, a ViewPluginComposite cannot include another ViewPluginComposite as a child view plugin.

With a composite view plugin running in parallel mode, cutouts and visual feedback for all running child plugins are shown at the same time on the scan view. It’s a good idea to individually configure each child plugin so that it is visually distinct from all the others.

For instance, in a workflow in which multiple cutouts are simultaneously displayed on the scan view, you could adjust the cutouts so that they do not overlap each other, or, if they do overlap, adjust the appearance of the scan feedback in order for it to be clear to the user which scan feedback comes from which plugin.

You could also use the ALScanViewDelegate method scanView:updatedCutoutWithPluginID:frame: to position labels near each cutout so that the user knows what to scan with each one.

ALViewPluginComposite

ALViewPluginComposite is the class that represents a view plugin composite. Each composite consists of:

  • the composite’s plugin ID

  • the processing mode, parallel or sequential

  • a list of children view plugins

  • a delegate object (of type ALViewPluginCompositeDelegate)

  • a map of String → ScanViewPluginConfig for each child plugin

Configuring a plugin composite with JSON

A JSON configuration for a plugin composite looks like this:

{
    "viewPluginCompositeConfig": {
        "id": "parallel_composite_scanning",
        "processingMode": "sequential",
        "viewPlugins": [
            {
                "viewPluginConfig": {
                    "pluginConfig": {
                        "id": "licensePlate",
                        "licensePlateConfig": {
                        "scanMode": "auto"
                        }
                    }
                },
                "viewPluginConfig": {
                    "pluginConfig": {
                        "id": "vin",
                        "vinConfig": { }
                    }
                }
            }
        ]
    }
}
  • viewPluginCompositeConfig should be the root node of the JSON config.

  • processingMode should be either sequential or parallel, depending on the operating mode desired.

  • id should be a valid keyword string and is a required field.

  • viewPlugins is an array where the viewPluginConfig JSON bodies of each child plugin is listed.

To create a plugin composite with this, use the initWithJSONDictionary:error: initializer:

  • Swift

  • Objective-C

do {
    let jsonConfig = self.getJSONConfigString()
    let configDictionary = try jsonConfig.toJSONObject()
    let pluginComposite = try ALViewPluginComposite.init(jsonDictionary: configDictionary)
    pluginComposite.delegate = self // implement the ALViewPluginCompositeDelegate
} catch {
    // handle errors
}
NSError *error;
NSString *jsonConfig = [self getJSONConfigString];
NSDictionary *configDictionary = [jsonConfig toJSONObject:&error];
// check error

ALViewPluginComposite *pluginComposite = [[ALViewPluginComposite alloc] initWithJSONDictionary:JSONDict
                                                                                         error:&error];
// check error
pluginComposite.delegate = self; // implement the ALViewPluginCompositeDelegate

Then, pass the composite to the initializer of ALScanView:

  • Swift

  • Objective-C

do {
    let scanView = ALScanView(frame: frame, scanViewPlugin: pluginComposite, scanViewConfig: scanViewConfig)
} catch {
    // handle errors
}
ALScanView *scanView = [[ALScanView alloc] initWithFrame:frame
                                          scanViewPlugin:pluginComposite
                                          scanViewConfig:scanViewConfig
                                                   error:&error];

Configuring a plugin composite programmatically

To create a plugin composite without JSON, provide the children view plugins to ALViewPluginComposite 's initializer as an array, as follows:

  • Swift

  • Objective-C

let children: [ALScanViewPlugin] = [ meterScanViewPlugin, barcodeScanViewPlugin ]
let pluginComposite = try ALViewPluginComposite(id: "parallel-meter-barcode",
                                                mode: .parallel,
                                                children: children)
pluginComposite.delegate = self
NSError *error;
NSArray<ALScanViewPlugin *> *children = @[ meterScanViewPlugin, barcodeScanViewPlugin ];
ALViewPluginComposite *pluginComposite = [[ALViewPluginComposite alloc] initWithID:@"parallel-meter-barcode"
                                                                              mode:ProcessingModeParallel
                                                                          children:children
                                                                             error:&error]; // check error
pluginComposite.delegate = self; // implement the ALViewPluginCompositeDelegate

Reading the result

With composite plugins, once all the results have been scanned, the ALViewPluginCompositeDelegate method viewPluginComposite:allResultsReceived: is called on the delegate, providing a list of ALScanResult found for each child plugin, in the order in which they were scanned.

(In a composite with the parallelFirstScan processing mode, this result would have been for the first plugin successfully scanned, and nothing else.)

Individually listening to each child plugin’s result

On top of viewPluginComposite:allResultsReceived:, you can still determine the moment when a certain child plugin has returned a scan result, by setting its scan plugin delegate.

  • Swift

  • Objective-C

let children: [ALScanViewPlugin] = [ meterScanViewPlugin, barcodeScanViewPlugin ]
let pluginComposite = // ... create the composite passing in `children`
pluginComposite.delegate = self // implement the ALViewPluginCompositeDelegate

// attach to each scan plugin's delegate
meterScanViewPlugin.scanPlugin.delegate = self
barcodeScanViewPlugin.scanPlugin.delegate = self

// implement ALScanPluginDelegate method(s)
func scanPlugin(_ scanPlugin: ALScanPlugin, resultReceived scanResult: ALScanResult) {
    if scanPlugin == meterScanViewPlugin.scanPlugin {
        // here you have the scan result of the meter plugin
    } else if scanPlugin == barcodeScanViewPlugin.scanPlugin {
        // here you have the scan result of the barcode plugin
    }
}
NSArray<ALScanViewPlugin *> *children = @[ meterScanViewPlugin, barcodeScanViewPlugin ];
ALViewPluginComposite *pluginComposite = // ... create the composite passing in `children`
pluginComposite.delegate = self; // implement the ALViewPluginCompositeDelegate

// attach to each scan plugin's delegate
meterScanViewPlugin.scanPlugin.delegate = self;
barcodeScanViewPlugin.scanPlugin.delegate = self;

// implement ALScanPluginDelegate method(s)
- (void)scanPlugin:(ALScanPlugin *)scanPlugin resultReceived:(ALScanResult *)scanResult {
  if (scanPlugin == meterScanViewPlugin.scanPlugin) {
    // here you have the scan result of the meter plugin
  } else if (scanPlugin == barcodeScanViewPlugin.scanPlugin) {
    // here you have the scan result of the barcode plugin
  }
}

Example JSON

Here you will see an example JSON configuration for a composite scan setup, using a license plate and VIN plugins as children running in parallel mode:

{
  "cameraConfig": {
    "captureResolution": "1080p",
    "pictureResolution": "1080p"
  },
  "flashConfig": {
    "mode": "manual_off",
    "alignment": "bottom_left"
  },
  "viewPluginCompositeConfig": {
    "id": "com.anyline.configs.plugin.parallel-lp-vin",
    "processingMode": "parallel",
    "viewPlugins": [
      {
        "viewPluginConfig": {
          "pluginConfig": {
            "id": "com.anyline.configs.plugin.lp",
            "licensePlateConfig": {
              "scanMode": "auto"
            }
          },
          "cutoutConfig": {
            "style": "rect",
            "maxWidthPercent": "85%",
            "maxHeightPercent": "100%",
            "alignment": "top_half",
            "width": 0,
            "ratioFromSize": { "width": 4, "height": 1 },
            "offset": { "x": 0, "y": 0 },
            "strokeWidth": 2,
            "cornerRadius": 10,
            "strokeColor": "8be9fd",
            "outerColor": "000000",
            "outerAlpha": 0,
            "feedbackStrokeColor": "0099FF"
          },
          "scanFeedbackConfig": {
            "style": "rect",
            "strokeWidth": 2,
            "strokeColor": "0099FF",
            "fillColor": "330099FF",
            "cornerRadius": 0,
            "beepOnResult": false,
            "vibrateOnResult": false,
            "blinkAnimationOnResult": true
          }
        }
      },
      {
        "viewPluginConfig": {
          "pluginConfig": {
            "id": "com.anyline.configs.plugin.vin",
            "vinConfig": {}
          },
          "cutoutConfig": {
            "style": "rect",
            "maxWidthPercent": "85%",
            "alignment": "top_half",
            "ratioFromSize": { "width": 62, "height": 9 },
            "offset": { "x": 0, "y": 240 },
            "outerColor": "000000",
            "outerAlpha": 0,
            "strokeWidth": 2,
            "strokeColor": "ff79c6",
            "cornerRadius": 4,
            "feedbackStrokeColor": "0099FF"
          },
          "scanFeedbackConfig": {
            "style": "animated_rect",
            "animation": "traverse_multi",
            "animationDuration": 250,
            "strokeWidth": 2,
            "strokeColor": "0099FF",
            "fillColor": "220099FF",
            "beepOnResult": false,
            "vibrateOnResult": false,
            "blinkAnimationOnResult": true
          }
        }
      }
    ]
  }
}