Skip to main content
This tutorial picks up where Part 1 left off. You already have a working iOS app that runs a GBG Go identity journey with stub camera views. Now you will replace those stubs with the real Smart Capture SDKs: guided document scanning, face capture with liveness detection with encrypted biometric blobs, and passport chip reading over NFC. The change is small. The bridge architecture you built in Part 1 was designed so that swapping camera views is a minimal edit. The handler setup, awaitCompletion()/complete() pattern, and all bridge wiring stay the same. Only the views inside fullScreenCover change, plus one new sheet for the chip reader.
Reference app: The complete source code is on the part-2-smart-capture branch of the reference repository. To see exactly what changed from Part 1, diff the branches:

Prerequisites

Everything from Part 1, plus:

Required frameworks

Contact your GBG account representative to obtain these frameworks: Two open-source Swift packages are needed at runtime as well, pinned to the exact versions the SDKs were built against:
Each SDK links its dependencies at runtime: FaceCamera needs IDLiveFaceCamera and IDLiveFaceIAD, Document needs IDSCamera and Lottie, and OzoneNFC needs OpenSSL. Miss any of them and the app crashes at launch with a “Library not loaded” error.

Start from Part 1

Check out the Part 2 branch:
Or if you already have the repo:
The companion server is unchanged — start it the same way as Part 1:

Add the Smart Capture SDKs

The Smart Capture SDKs ship as six .xcframework bundles plus two Swift packages. Drop the frameworks into the project, embed and sign them, add the packages and the NFC capability, then enable the compiler flag. The rest of the section walks through each step.

1. Place the frameworks

Copy all six .xcframework bundles into GBGGoReference/Frameworks/ (the reference branch gitignores .xcframework bundles there, so they are never committed):

2. Add to Xcode

  1. Open GBGGoReference/GBGGoReference.xcodeproj in Xcode.
  2. Select the GBGGoReference target.
  3. Go to General > Frameworks, Libraries, and Embedded Content.
  4. Click +, then Add Other > Add Files.
  5. Select all six .xcframework bundles from the Frameworks/ directory.
  6. Set each to Embed & Sign.
The project already includes $(PROJECT_DIR)/Frameworks in its framework search paths. You only need to add the frameworks to the target’s embedded content.

3. Add the Swift packages

  1. File > Add Package Dependencies…
  2. Add https://github.com/airbnb/lottie-spm.git with the Exact Version rule set to 4.6.0.
  3. Add https://github.com/krzyzanowskim/OpenSSL.git with the Exact Version rule set to 3.6.3000.
Use exact versions. A version the SDKs were not built against is how runtime crashes on open happen.

4. Add the NFC capability

Chip reading uses Core NFC, which needs an entitlement:
  1. Select the GBGGoReference target, then Signing & Capabilities.
  2. Click + Capability and add Near Field Communication Tag Reading.
The two Info.plist entries chip reading needs are already on the reference branch. If you are building your own project, add them:
A0000002471001 is the application identifier of the passport’s data application. Without it, iOS cannot select the passport application and every read fails.

Enable the Compiler Flag

The Smart Capture integration is gated behind a compile-time flag called SMART_CAPTURE_ENABLED. This flag is off by default, so the app builds and runs with stubs even if you haven’t added the frameworks yet.
  1. In Xcode, select the GBGGoReference target.
  2. Go to Build Settings and make sure “All” is selected, not “Basic”.
  3. Search for Active Compilation Conditions (SWIFT_ACTIVE_COMPILATION_CONDITIONS).
  4. Add SMART_CAPTURE_ENABLED to both the Debug and Release configurations.
With the flag set, the app compiles the Smart Capture wrapper views and uses them on a physical device. On Simulator, it still falls back to stubs regardless of the flag.

Document Capture with SmartCapture

Create SmartCaptureDocumentView.swift in the Sources/Capture/ group. This is a thin SwiftUI wrapper around the Document SDK:
The entire file is wrapped in #if SMART_CAPTURE_ENABLED. When the flag is off, this file is invisible to the compiler — no import Document, no dependency on the framework.

How it works

  • DocumentSDK is a @StateObject that manages the camera session and document detection.
  • DocumentScannerConfig controls capture behaviour:
    • autoCaptureToggleConfig: .showDelayed(durationMs: 10_000) — shows a manual capture button after 10 seconds if auto-capture hasn’t triggered.
    • documentSide — which side of the document to capture (.front or .back).
    • documentType — classification hint (.passport, .idcard, .unknown, etc.).
  • sdk.mainView renders the camera viewfinder with real-time document detection overlays.
  • sdk.$documentScannerResult publishes when the SDK completes — either a success with image data or a failure with a message.

What you get vs the stub

Face Capture with Liveness

Selfie capture follows the same pattern as document capture: a SwiftUI wrapper around the SDK’s view that builds a SelfieCaptureResult from the raw output and forwards it to the bridge slot. The wrapper handles SDK initialisation, runs the liveness flow, and surfaces failures as recoverable bridge errors. Create SmartCaptureFaceView.swift in the Sources/Capture/ group:

How it works

The FaceCamera SDK uses a delegate pattern instead of Combine publishers:
  • FaceCameraSDK.controllerSwiftUIWrapper(delegate:) returns a SwiftUI view wrapping the face capture controller.
  • FaceCameraDelegateHandler implements FaceCameraListenable and forwards each callback to a closure.
  • didCapture delivers three values:
    • previewPhoto — a UIImage for display in the app.
    • encryptedBlob — encrypted biometric data for server-side liveness verification.
    • unencryptedBlob — unencrypted biometric data.
  • didEncounterError fires when capture fails (e.g. camera hardware issue).
  • didCancel and didTapBack both fire when the user dismisses.

What you get vs the stub

The stub views return the raw JPEG data in both encryptedBlob and unencryptedBlob as a placeholder. This is structurally valid so the bridge protocol works, but it will not pass server-side liveness verification. The real FaceCamera SDK produces properly encrypted biometric data.

Passport Chip Reading

GBGBridge 1.2.0 adds a third typed slot, host.nfcRead, alongside documentCapture and selfieCapture. It answers the document.nfc capability and the document.nfc.read action, and follows the same awaitCompletion() / complete() pattern. The app does two things: it tells the SDK whether this device can read a chip, and it runs the SmartCapture Ozone reader when a request arrives. The SDK does the rest. It checks the request carries the access key (document number, date of birth, date of expiry), answers BUSY and UNSUPPORTED itself, decides from the BAC and PACE outcomes whether the chip actually opened, reads the MRZ out of DG1, and shapes the response the web journey expects. The Passport Chip Reading guide has the full contract and error codes.

Wire the slot

In configureHandlers(), after the two camera handlers:
deviceCanRead matters. The SDK does not link Core NFC, so it cannot tell whether the device has a reader. It defaults to false, which reports chip reading as unsupported. NFCTagReaderSession.readingAvailable is false on iPads and the Simulator, so those devices report unsupported too, and the web journey offers the person another way to continue. Import CoreNFC under the same #if.

Present the reader

Chip reading is presented as a sheet rather than a full-screen cover, so the person can always swipe it away. Dismissing it answers the read as cancelled; cancelIfBusy is a no-op if the read already finished.

Run the Ozone reader

Create Sources/Capture/SmartCaptureChipReaderView.swift. It wraps OzoneNFCViewController, which draws the whole chip-read experience (where the chip sits, progress, errors), and maps its result onto the SDK’s NfcReadResult:
OzoneChipResult.map turns a successful read into .read(files:checks:), with every data group the chip returned as an NfcReadFile and the BAC, PACE, chip authentication, active authentication and passive authentication outcomes as NfcAuthChecks. userCancelled becomes .cancelled, invalidNfcKeyFormatting becomes .failed(.missingMrzData, ...), and anything else .failed(.nfcReadFailed, ...). This is the same file as on the reference branch; the Capability Handling guide shows the same mapping as a standalone function.

No stub for chip reading

The SDK ships stub camera views but no stub chip reader: a fake reader cannot produce a chip the platform would accept. Without the flag, or on Simulator, the slot has no handler, the SDK reports chip reading as unsupported, and the journey offers its own alternative. That is the intended behaviour, not a gap.

The Swap Pattern

Open JourneyView.swift. The camera view computed properties now use conditional compilation to choose between stubs and real SDKs:
The selfie camera follows the same pattern:

Why #if instead of runtime switching?

Conditional compilation (#if) has three advantages over a runtime toggle:
  1. Zero overhead. When the flag is off, the Smart Capture code does not exist in the binary. There are no unused framework imports and no dead code.
  2. No accidental dependency. Without the flag, the project compiles without the Smart Capture frameworks. A runtime check would still require the frameworks to be linked.
  3. Clear separation. The #if/#else blocks make it obvious which code path runs in each configuration. Reviewers see the stub and real implementations side by side.

What didn’t change

Look at what surrounds the #if blocks — nothing changed:
  • The BridgeHost initialization is identical.
  • The handler assignment in configureHandlers() is identical.
  • The onChange listeners driving fullScreenCover presentation are identical.
  • Chip reading is a third slot alongside the two camera slots, wired the same way; nothing in the camera wiring changed.
  • The bridge protocol, message format, and server code are all identical.
This is the whole point of the architecture. The bridge integration layer is stable. Only the capture views swap.

Camera Permissions

Part 2 also adds permission state detection in configureHandlers():
CameraDetector.check() queries the device’s camera hardware availability and permission state. Setting permissionState on the typed slots means the bridge’s built-in capability.query handler can report accurate permission information to the web journey — allowing the journey to adapt its flow if camera access is denied or restricted.

Test on a Physical Device

  1. Make sure the companion server is running.
  2. Find your Mac’s local IP:
  3. Connect a physical iOS device and select it in Xcode.
  4. Press Cmd+R to build and run.
  5. On the Setup screen, enter http://<your-mac-ip>:3000 as the server URL.
  6. Tap Start Journey.
  7. When the journey requests a document capture, the SmartCapture document scanner appears — with a guided overlay, real-time edge detection, and auto-capture.
  8. When the journey requests a selfie, the FaceCamera SDK appears — with face positioning guidance and liveness detection.
  9. When the journey asks to read the passport chip, the Ozone reader appears as a sheet. Hold the passport against the top of the phone until the read completes.
On Simulator, stubs are used automatically and chip reading is reported as unsupported. This is by design.

Common Pitfalls

Missing runtime dependencies

If you add FaceCamera.xcframework but forget IDLiveFaceCamera.xcframework or IDLiveFaceIAD.xcframework, the app crashes at launch:
The same happens for IDSCamera or Lottie (needed by Document) and for OpenSSL (needed by OzoneNFC). Add every framework and both Swift packages to fix this.

Flag not set

If you add the frameworks but forget to set SMART_CAPTURE_ENABLED in Active Compilation Conditions, the app compiles and runs — but silently uses stubs. There is no error. Check Build Settings if the Smart Capture views are not appearing.

Simulator with flag enabled

Even with SMART_CAPTURE_ENABLED set, the Simulator always uses stubs. The #if !targetEnvironment(simulator) condition ensures this. Smart Capture SDKs require real camera hardware.

Framework signing

All six frameworks must be set to Embed & Sign in the target’s Frameworks, Libraries, and Embedded Content. “Embed Without Signing” or “Do Not Embed” causes runtime crashes.

Chip reading reported as unsupported

The journey says this device cannot read the chip. Three causes, in order of likelihood: the device is an iPad or the Simulator (no NFC reader, expected); deviceCanRead was never set, so it kept its default of false; or the flag is off, so the slot has no handler. If the reader opens but fails immediately, check the Near Field Communication Tag Reading capability is on the target and the A0000002471001 select identifier is in Info.plist.

What’s Next

  • API Reference — Full documentation for BridgeHost, CaptureCapability, and result types.
  • Stub Camera Views — Details on the stubs and the swap pattern.
  • Capability Handling — Deep dive into typed slots, custom capabilities, and permission states.
  • Passport Chip Reading: The full nfcRead contract, the complete Ozone result mapping, and the error codes the web journey can receive.