Mobile Capture 2 SDK for iOS

 

Downloading the SDK

 

Download the Mobile Capture 2 SDK (version 2.2.4) (Swift 4.0.3) here.

 

Download the Mobile Capture 2 SDK (version 2.2.4) (Swift 4.1) here.

 

Download the Mobile Capture 2 SDK (version 2.2.7) (Swift 5) here.

 

Download the Mobile Capture 2 SDK (Swift 5.1) here.

 

Download the Mobile Capture 2 SDK (Xcode 11.5, Swift 5.2) here.

 

Download the Mobile Capture 2 SDK (Xcode 11.6) here

 

Download the Mobile Capture 2 SDK (Xcode 12) here

 

Download the Mobile Capture 2 SDK (Xcode 12..0.1) here (updated 14 Oct 2020)

 

Including the SDK

 

 

ClientConnector startup

In your AppDelegate class, create a ClientConnector instance variable by using the following:

let clientConnector = ClientConnector(backgroundIdentifier: "com.yourcompany.yourapplication")

 

The backgroundIdentifier parameter has to be a unique string for your application, it is used to connect running processes.

To make sure it is unique to your application, we advise to set it to your package name.

 

To reach the ClientConnector in other classes we use the following:

First get a reference to the AppDelegate class...

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate

...Then access the ClientConnector

appDelegate.clientConnector

 

 

NOTES

All ClientConnector background process callbacks are NOT passed back to the main thread. Make sure that when you want to make changes to the user interface in a background process callback, you do this on the main thread by using the following

dispatch_async(dispatch_get_main_queue(), {
    // Example
    self.performSegueWithIdentifier("segueName", sender: self)
})

 

Methods that communicate with the IConnector are all overloaded to allow for additional headers. For example:

loadFieldDefinitionsForDocument(documentStateUniqueId: String, finished: (error: CPError?, uploadResponse: String?) -> Void)
loadFieldDefinitionsForDocument(documentStateUniqueId: String, additionalHeaders: [String: String], finished: (error: CPError?, uploadResponse: String?) -> Void)

 

 

 

Login

There are two ways of logging in:

  1. Using login credentials (login name + password)
  2. Using your Admin Panel organization's Unique ID (configuration unique id)

 

login()

When using login credentials you should use the function login

login(loginName: String, password: String, finished: (error: CPError?, docTypesJSON: [NSDictionary]?) -> Void)

 

An example would be:

appDelegate.clientConnector.login(loginName: "Name", password: "myPassword", finished: { (error, docTypesJSON) in
    if error != nil {
        // Show error message
        let message = error!.message
    } else {
        // If there are multiple document types available
        if docTypesJSON != nil {
            // Provide an option to select the correct document type. 
        } else {
            // Select document type index 0
        }
    }
})

 

Providing a user a way to select a document type can be done by using the docTypesJSON data or more easily, by using several ClientConnector functions

// Amount of document types
let docCount: Int = appDelegate.clientConnector.currentDocumentTypesCount

// Get a specific document type display name
let docDisplayName: String = appDelegate.clientConnector.currentDocumentTypeDisplayName(index: Int)

// Select the wanted document type
clientConnector.selectDocumentTypeIndex(index) { (error) in
    dispatch_async(dispatch_get_main_queue(), {
        if error != nil {
            // Show error
        } else {
            // Success
        }
    })
}

 

 

loadConfiguration()

When using a specific configuration unique id, the following function should be used:

loadConfiguration(configurationUniqueId: String, finished: ( error: CPError?, docTypesJSON: [NSDictionary]?) -> Void )

 

An example would be:

appDelegate.clientConnector.loadConfiguration(configurationUniqueId: "1234-abc-4321-etc", finished: { (error, docTypesJSON) in
    if error != nil {
        // Show error message
        let message = error!.message
    } else {
        // If there are multiple document types
        if docTypesJSON != nil {
            // Provide an option to select the correct document type.
        } else {
            // Select document type index 0
        }
    }
})

 

 

Load configuration from String

There is also an option to load the desired configuration from a string in JSON format. To use this functionality you can use the loadConfigurationFromString method:

loadConfigurationFromString(configuration: String, finished: (error: CPError?) -> Void)

 

 

 

 

Camera

Next to the ClientConnector, the SDK contains a class named CPCameraViewController. This class extends UIViewController and contains all the image taking functionality.

 

It is advised to create an empty view controller in your storyboard and create a class named (for example): CameraContainerController. CameraContainerController extends UIViewController. Set the view controller's custom class to your newly created CameraContainerController.

In your storyboard view, include a container view and make it have an aspect ratio of 3:4 (3 wide, 4 high). This aspect ratio is important for correct image taking functionality

Including the container view automatically created an empty view controller the same size as the container view. Give this new view controller the custom class: CPCameraViewController.

Give the automatically created embed segue a unique identifier. For example: camera. We'll use this segue to reference the CPCameraViewController.

In CameraContainerController create an instance variable that will hold a reference to the CPCameraViewController:

var cameraViewController: CPCameraViewController!

 

You can set this reference by using the container's embed segue as followed:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    switch segue.identifier! {
    case "camera":
        cameraViewController = segue.destinationViewController as! CPCameraViewController
    default:
        break
    }
}

 

Some settings can be provided to alter the behaviour of the cameraViewController:

cameraViewController.insetHorizontal: CGFloat
cameraViewController.insetTop: CGFloat
cameraViewController.insetBottom: CGFloat

cameraViewController.cropViewDisableZoom: Bool
cameraViewController.cropViewGapLeft: Int32
cameraViewController.cropViewGapTop: Int32
cameraViewController.cropViewGapRight: Int32
cameraViewController.cropViewGapBottom: Int32

cameraViewController.borderCornerIndicatorsVisible: Bool
cameraViewController.borderCornerIndicatorsNoMatchColor: UIColor
cameraViewController.borderCornerIndicatorsMatchColor: UIColor

cameraViewController.showDetectedBoundaries: Bool
cameraViewController.boundaryNoMatchColor: UIColor
cameraViewController.boundaryMatchColor: UIColor

cameraViewController.takePictureSetsCropCorners: Bool

 

The SDK provides three ways of capturing an image:

  1. Automatic image capture
  2. Manual image capture
  3. Importing an image

 

For automatic capture, a callback of CPCameraViewController needs to be implemented:

In CameraContainerController set the autoSnap callback as followed:


cameraViewController.autoSnap = {
    self.cameraViewController.takePicture { (foundPage) in
        if foundPage {
            self.imageTaken() // Create imageTaken function in this class (See below)
        } else {
            // Page not found / borders not detected
        }
}

 

For manual capture, create a capture button in your view and link it as an @IBAction in your CameraContainerController. When clicked, call takePicture as followed:

@IBAction func cameraButtonClick(sender: AnyObject) {
    cameraViewController.takePicture { (foundPage) in
        // Handle it the same as an automatic capture by checking foundPage and then calling imageTaken()...
        // ..Or give a user the option to review and crop the image regardless of foundPage (See startCrop() below)
    }
}

 

To give a user the ability to review and crop an image, create a function as below:

func startCrop() {
    cameraViewController.stopCamera()
    cameraViewController.stopAppActivationCameraListener()

    cameraViewController.showCrop()
}

 

When a user is done with setting the cropping border, call imageTaken() and reset the camera as followed:

@IBAction func cropButtonClick(sender: AnyObject) {
    imageTaken() // Create imageTaken function in this class (See below)
    
    // Reset the camera
    cameraViewController.startCamera()
    cameraViewController.startAppActivationCameraListener()
    cameraViewController.hideAndClearCrop()
}

 

To cancel cropping an image:

func endCrop(startCamera startCamera: Bool) {
    if (startCamera) {
        cameraViewController.startCamera()
    }

    cameraViewController.startAppActivationCameraListener()
    cameraViewController.hideAndClearCrop()
    cameraViewController.resetBorderDetection()
}

 

The imageTaken function handles what happens after an image is ready. It is up to the flow of the application what happens next. See the next sections about previewing, indexing and uploading for next possible steps.

 

 

To import an image, the SDK provides the method: "importImage" which expects a UIImage. The following code snippet will show an example implementation:

 

@IBAction func importClick(_ sender: AnyObject) {

    cameraViewController.stopCamera()

    cameraViewController.stopAppActivationCameraListener()

 

    let imagePicker = UIImagePickerController()

    imagePicker.navigationBar.isTranslucent = false

    imagePicker.navigationBar.barTintColor = navigationController!.navigationBar.barTintColor // Background color

    imagePicker.navigationBar.tintColor = UIColor.white // Cancel button ~ any UITabBarButton items

    imagePicker.navigationBar.titleTextAttributes = [

        NSAttributedStringKey.foregroundColor : UIColor.white

    ]

    imagePicker.delegate = self

    imagePicker.sourceType = .photoLibrary

        

    present(imagePicker, animated: true, completion: nil)

}

    

@objc func imagePickerController(_ picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) {

    cameraViewController.importImage(image) {

        self.startCrop()

    }

    

    picker.dismiss(animated: true, completion: nil)

}

 

Previewing images

Create a storyboard layout that can contain a variable amount of UIImageView's. You can get the amout of captured imaged in the current document by calling:

selectedDocumentNumberOfPages

 

Now you can set the thumbnail images of the UIView's by using:

selectedDocumentPreviewPage(index: Int)

 

The full size images are only available through an image viewer in the SDK. You can load and display the full size image as followed:

let imageViewer = ImageViewer(clientConnector: appDelegate.clientConnector, pageIndex: index, displacedView: UIImageView, imageSize: CGSizeMake(view.frame.width, view.frame.height - 120))

// Function presentImageViewer is an extension of UIViewController
self.presentImageViewer(imageViewer)

 

The ClientConnector also contains several functions to handle images:

selectedDocumentRemovePage(index: Int)
selectedDocumentRotatePage(index: Int)
selectedDocumentMovePage(fromIndex: Int, toIndex: Int)

 

 

 

Indexing

Get all field definitions for the current document state by using

let fieldDefinitions: [FieldDefinition] = selectedDocumentFieldDefinitions

Or all field definitions for a specific document state by using:

// Note: get the documentTypeIndex of the documentState by using: documentTypeIndexForDocumentState(documentStateUniqueId: String)

let fieldDefinitions: [FieldDefinition] = fieldDefinitionsForDocumentState(documentStateUniqueId: String, documentTypeIndex: Int, includeHiddenFields: Bool = true)

 

Get the definition's datatype by using

fieldDefinitions[index].datatype

 

Data type can be one of the following:

  1. "string"
  2. "number"
  3. "amount"
  4. "date"
  5. "datetime"
  6. "lookup"
  7. "boolean"
  8. "barcode"

 

You can get data directly from one of the field definitions in the array of field definitions  or for instance get the field value by using:

documentFieldValue(clientConnector.currentDocumentStateUniqueId, name: fieldDefinition.name)

 

A field value can be set by using

selectedDocumentSetFieldValue(fieldDefinition.name, value: "example")

 

After every change a function needs to be called to check if fields need to be updated:

clientConnector.updateFieldsAfterChange { (changed, error, fieldMessages) in
    dispatch_async(dispatch_get_main_queue(), {
        if error != nil {
               
        } else {
            self.fieldMessages = fieldMessages
            self.tableView.reloadData()
        }
    })
}

 

When done with indexing, the fields should be validated:

validateDocumentFields(documentStateUniqueId) { (error, uploadResponse, fields) in
    dispatch_async(dispatch_get_main_queue(), {
        if error != nil {
            // Show error
        } else if fields?.count > 0 {
            // One or more field values are not correct
        } else {
            // Valid
        }
    })
}

 

 

 

 

Uploading

If the user has images in his current document, you can make him call the upload function. The upload function only requires to pass the unique ID of the document you wish to upload. It also has a callback that passes the upload progress back to it and a finished callback.

 

To upload the current document you can get the current document unique ID and pass that to the upload function as followed:

let currentUniqueId = appDelegate.clientConnector.currentDocumentStateUniqueId

// Direct upload without further feedback
upload(currentUniqueId)

// Document is uploading, prepare a new document:
createDocumentState(true)


// Or use one of the overloads of the upload method, for example:

upload(documentStateUniqueId: String, additionalHeaders: [String:String], allowCellularData: Bool, progressHandler: { (progress) in 
        print("Progress: (progress)")
    }, finished: { error, response in
        if error != nil {
            print("Upload error: (error)")
        } else {
            print("Upload success. Response: (response)")
        }
    }   
)

 

         // There is a seperate method to upload a pdf:

         uploadPDF(pdfFilePath: String,

                   documentStateUniqueId: String,

                   additionalHeaders: [String:String],

                   allowCellularData: Bool,

                   progressHandler: ((progress: Float) -> Void)? = nil,

                   finished: ((error: CPError?, uploadReponseJSON: NSDictionary?) -> Void)? = nil) ;

 

You can register to a progress callback to get the progress of a running upload at any time:

registerProgressListener(key: String, listener: { (notificationUniqueId, progress) in
    dispatch_async(dispatch_get_main_queue(), {
        // Do something with the progress received for notificationUniqueId
    })
})

 

 

 

Notifications

Register to a notifications changed callback to update the screen when receiving a notification:

registerNotificationsChangedListener(key: String, listener: { badgeCount in
    dispatch_async(dispatch_get_main_queue(), {
        self.tableView.reloadData()
    })
})

 

Get a specific notification:

// SortingValue can also be: createdDateTime, title, longDescription, unread or status: NotificationStatus
notificationForIndex(index, sortingValue: "displayDateTime")

 

Possible notification statusses are:

  1. New
  2. InProgress
  3. SavedSession
  4. WaitingForUpload
  5. WifiPending
  6. Uploading
  7. Failed
  8. Retry
  9. Uploaded
  10. Edit
  11. NotificationMessage
  12. EditMessage
  13. Unknown

 

A running upload (Status Uploading) can be cancelled by using:

cancelUpload(notificationUniqueId: String)

 

A notification in status SavedSession can be put back into the current session by using:

switchToDocumentState(documentStateUniqueId: String)

 

A notification in status Edit or EditMessage can be restored into the current session by using:

restoreDocumentState(documentStateUniqueId: String, forNotificationUniqueId: String)

 

A failed upload (Status Failed) or a status Retry or a status WifiPending can be retried. Simply upload it again:

upload(documentStateUniqueId, allowCellularData: Bool)

 

If a document is waiting to be uploaded (WaitingForUpload) (This is usually when a document was finished without internet connection):

validateDocumentFields(documentStateUniqueId) { (error, uploadResponse, fields) in
    dispatch_async(dispatch_get_main_queue(), {
        if error != nil {
            // Show error
        } else if fields?.count > 0 {
            // switchToDocumentState to clear field errors
        } else {
            // retryUpload(documentStateUniqueId)
        }
    })
}

 

 

 

 

Additional information

 

General usage and naming conventions

A  Configuration  is the object containing the settings for a specific user or organization. A  Configuration  can be set using the login, loadConfiguration or loadConfigurationFromString methods. A  Configuration  contains information about the right server and which document types the user is allowed to use.

 

On first initialization the ClientConnector creates a  DocumentState  and places this in the  CurrentState . It will also create a  Notification  with a reference to that  DocumentState .

A  Notification  is created to show inside a message / history list. It contains information about a specific  DocumentState  and a reference back to it. A  DocumentState  always has specific a  Notification  related to itself (including it's status ('New', 'InProgress', 'WaitingForUpload', etc)) but another  Notification  for a  DocumentState  can also relate to a message received from the server. This way, a  DocumentState  can contain multiple server notifications but always only one related to the status of the initial document. A server  Notification  can be recognized by it's status: 'NotificationMessage' or 'EditMessage'.

 

There is always only one  CurrentState  which contains the  User  associated to it and the current  DocumentState . This  CurrentState  allows the user to continue where he left off the last time he used the application.

A  DocumentState  contains a unique identifier to be able to track it, a  Configuration , a  Document  and it's  Notifications .

A  Document  contains information about the images the user is trying to process such as: it's document type index in relation to the document types loaded into the  Configuration , a list of  Pages , a list of  Fields  and a list of HiddenField names. 

A  Page  contains the image the user has captured.

A  Field  contains the name and value of the indexing field, this is essentially the meta data that is uploaded along with the document.

 

 

 

 

Certificate pinning

The ClientConnector provides the ability to set a sessionDidReceiveAuthenticationChallengeHandler. A developer can use this callback as he wants to implement desired behaviour.

public var sessionDidReceiveAuthenticationChallengeHandler: 
    ((challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) -> Void)? = nil;

 

 

 

 

 

 

 

 

Create your own Knowledge Base