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)
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)
There are two ways of logging in:
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)
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:
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)
}
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)
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:
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
}
})
}
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
})
})
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:
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)
}
})
}
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.
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;