Captur iOS SDK: Implementation Guide
Alpha. The public API may change in a breaking way before general availability. Ensure you pin an exact version and check the changelog when you upgrade.
This guide covers a full integration: install the package, set up your API key, prepare a session, show the camera, handle events, and tear down.
Requirements
| Requirement | Minimum |
|---|---|
| iOS deployment target | iOS 15.0 |
| Xcode | 16 or newer |
| Swift | 5 language mode (5.5+ toolchain) |
| Distribution | Swift Package Manager (compiled XCFramework) |
The SDK is a binary XCFramework with ABI stability enabled. Nothing is built from source on your side.
1. Install the package
Xcode: File → Add Package Dependencies… and enter:
https://gitlab.development.captur.ai/captur/mobile-sdks/captur-mobile-ios-sdk.git
Or in Package.swift:
dependencies: [
.package(url: "https://gitlab.development.captur.ai/captur/mobile-sdks/captur-mobile-ios-sdk.git", exact: "<version>")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "CapturSDK", package: "captur-mobile-ios-sdk")
]
)
]
2. Add camera permission
Add a camera usage description to your app target's Info.plist:
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to capture and verify images.</string>
The SDK does not request camera permission for you. Request it in your app
before preparing the camera. If permission hasn't been granted,
prepareCamera throws an error (see Step 6).
3. Get an API key
Captur will issue an API key for your app. Pass it to the Captur initialiser
(see step 4).
4. Initialise the SDK
Create one Captur instance in a stable owner above the view hierarchy and keep it for the
lifetime of the app. In SwiftUI, the App type is a natural place to own it:
import CapturSDK
import SwiftUI
@main
struct ExampleApp: App {
private let captur = Captur(apiKey: "<YOUR_CAPTUR_API_KEY>")
var body: some Scene {
WindowGroup {
ContentView(captur: captur)
}
}
}
You can instead retain Captur in an app-owned view model, dependency container, or service layer.
Pass that same instance down the hierarchy.
5. Prepare a capture session
prepareSession resolves the policy and model for your policyType at the
given location, downloads the model if needed (an already-downloaded model is
reused, including recompiling it after an OS update), and returns a
CapturSession. It makes a network call, so call it
as early as you know a capture is coming, so the camera opens without a
cold start.
let session = try await captur.prepareSession(
policyType: "<YOUR_POLICY_TYPE>",
location: CapturLocation(latitude: 51.5074, longitude: -0.1278),
reference: "order-\(UUID().uuidString)"
)
| Parameter | Required | Description |
|---|---|---|
policyType | Yes | The Captur policy for this capture. |
location | Yes | Coordinate used to resolve the policy and model. A coarse, early location is fine here. |
reference | Yes | Your own identifier for this capture, used for billing and correlation. |
6. Prepare and show the camera
Ensure camera permission is granted, then call session.prepareCamera from
the main actor with the device's current location.
guard await AVCaptureDevice.requestAccess(for: .video) else {
// Tell the user camera access is required.
return
}
let cameraController = try await session.prepareCamera(
location: CapturLocation(latitude: 51.5074, longitude: -0.1278),
onCapturEvent: { event in
handleCapturEvent(event)
}
)
prepareCamera loads the session's inference and policy models into Core ML
and returns a CapturCameraController. It throws if either model fails to load
(CapturCameraError.modelLoadFailed) or camera permission is missing. If the
policy contract doesn't match, the SDK updates to the latest policy. A returned
controller means both models are loaded and ready.
The camera hardware itself isn't touched yet: AVFoundation capture starts when
you render the controller in a CapturCameraScreen.
Camera configuration
Pass a CapturCameraConfiguration to choose the initial camera position and
lens, and to control consecutive-pass automatic capture:
let configuration = CapturCameraConfiguration(
position: .back,
lens: .wide,
disableAuto: false
)
let cameraController = try await session.prepareCamera(
location: CapturLocation(latitude: 51.5074, longitude: -0.1278),
configuration: configuration,
onCapturEvent: { event in
handleCapturEvent(event)
}
)
| Setting | Options | Default | Description |
|---|---|---|---|
position | .back, .front | .back | Camera position to use when the preview starts. |
lens | .wide, .ultraWide | .wide | Lens to use when the preview starts. Availability depends on the selected position and device. |
disableAuto | true, false | false | When true, consecutive PASS predictions do not automatically finish the capture. Manual capture and the SDK-managed timeout still apply. |
You can omit configuration to use CapturCameraConfiguration.default. If
the requested position and lens combination is unavailable, camera activation
delivers a .failed event with CapturCameraError.lensUnavailable.
Torch and zoom are not initial configuration settings. Change them after the preview starts using the controller methods described in Step 7.
SwiftUI
Embed the preview and put your own controls on top:
import CapturSDK
import SwiftUI
struct CaptureScreen: View {
let cameraController: CapturCameraController
var body: some View {
ZStack {
CapturCameraScreen(capturCameraController: cameraController)
.ignoresSafeArea()
// Your capture controls overlay here.
}
}
}
7. Capture, inference & camera controls
Once CapturCameraScreen is on screen and frames are flowing, real-time
predictions arrive through the onCapturEvent callback you passed to
prepareCamera. Call captureImage() to take the final shot; the result
arrives through the same callback as .finalDecision. Calling
captureImage() before the preview is running throws
CapturCameraError.cameraPreviewNotRunning.
let cameraController = try await session.prepareCamera(
location: CapturLocation(latitude: 51.5074, longitude: -0.1278),
onCapturEvent: { event in
switch event {
case .prediction(let prediction):
// Live, per-frame result. `decision` carries `value`, `title` and `reasonCode`.
print(prediction.decision?.value ?? "pending")
case .finalDecision(let finalDecision):
// The captured outcome: `imageData`, plus `decision`, `modelVersionUID`, `prediction` and `trigger`.
print(finalDecision.imageData.count)
case .failed(let error):
print(error.localizedDescription)
@unknown default:
break
}
}
)
// Trigger a manual final decision.
try await cameraController.captureImage()
// Adjust the camera
try await cameraController.togglePosition() // front / back
try await cameraController.toggleLens() // wide / ultra-wide, etc.
try await cameraController.toggleTorch() // flash on / off
try await cameraController.toggleZoom() // cycle zoom levels
Event payloads
.prediction delivers a CapturPrediction; .finalDecision delivers a
CapturFinalDecision:
public struct CapturPrediction {
public let decision: CapturPredictionDecision?
}
public struct CapturPredictionDecision {
public let value: String // the policy decision value
public let title: String? // display title, when the policy provides one
public let reasonCode: String? // why the policy decided this, when provided
}
public struct CapturFinalDecision {
public let decision: CapturPredictionDecision?
public let modelVersionUID: String // the on-device model version that produced this decision
public let imageData: Data
public let trigger: CapturFinalDecisionTrigger
}
8. Retake or dismiss the camera
Three calls matter once a capture attempt ends:
try cameraController.retake()starts a fresh attempt after a final decision, clearing it and restarting the auto-capture timer. It only needs the preview to be running; no need to remove or pause the camera first.- Removing
CapturCameraScreenfrom the view hierarchy pauses the camera and inference but keeps the controller alive, so you can show a review UI and remount the same controller. Remounting before a final decision restarts the capture count and timeout; remounting after one does not start a new attempt, that takesretake(). await cameraController.close()is the terminal, idempotent shutdown: it releases the camera backend, stops callbacks, and marks the session closed, returning once shutdown completes. It does not dismiss your UI.
Only one camera preparation runs at a time per Captur instance; a new
preparation closes the previous controller before returning its replacement.
If a preparation is in flight and no longer needed, cancel its task. If it has
already returned but you'll never present the screen, close() the controller
before discarding it.
End-to-end example
import AVFoundation
import CapturSDK
import SwiftUI
@main
struct ExampleApp: App {
private let captur = Captur(apiKey: "<YOUR_CAPTUR_API_KEY>")
var body: some Scene {
WindowGroup {
ContentView(captur: captur)
}
}
}
struct ContentView: View {
let captur: Captur
@State private var session: CapturSession?
@State private var cameraController: CapturCameraController?
@State private var errorMessage: String?
var body: some View {
VStack {
Button("Prepare Session") {
Task { await prepareSession() }
}
Button("Prepare Camera") {
Task { await prepareCamera() }
}
.disabled(session == nil)
}
.fullScreenCover(isPresented: .constant(cameraController != nil)) {
if let cameraController {
ZStack(alignment: .topTrailing) {
CapturCameraScreen(capturCameraController: cameraController)
.ignoresSafeArea()
Button("Close") {
Task { await closeCapture() }
}
.padding()
}
}
}
}
@MainActor
private func prepareSession() async {
do {
let location = CapturLocation(latitude: 51.5074, longitude: -0.1278)
// Call prepareSession earlier in the flow so the model is prepared before capture.
let session = try await captur.prepareSession(
policyType: "<YOUR_POLICY_TYPE>",
location: location,
reference: "example-\(UUID().uuidString)"
)
self.session = session
} catch {
errorMessage = error.localizedDescription
}
}
@MainActor
private func prepareCamera() async {
guard let session else { return }
guard await AVCaptureDevice.requestAccess(for: .video) else {
errorMessage = "Camera permission is required."
return
}
do {
let controller = try await session.prepareCamera(
location: CapturLocation(latitude: 51.5074, longitude: -0.1278),
onCapturEvent: { event in
if case .finalDecision(let finalDecision) = event {
// Captured: `finalDecision.imageData` is the image, `finalDecision.decision`
// the outcome. Display or upload them here. See §7 for all event cases.
}
}
)
cameraController = controller
} catch {
errorMessage = error.localizedDescription
}
}
@MainActor
private func closeCapture() async {
await cameraController?.close()
cameraController = nil
session = nil
}
}
SDK version
For support requests and bug reports, read the bundled SDK version at runtime:
CapturSDKMetadata.version // the bundled marketing version string
CapturSDKMetadata.fullVersion // version with any prefix/suffix