GBGBridge ships two stub camera views — StubDocumentCameraView and StubSelfieCameraView — that let you prove the bridge works end-to-end without installing the SmartCapture SDKs.
Development and testing only. On a real device the stub views use UIImagePickerController to take a plain photo. On the Simulator they display programmatically drawn placeholder images with a shutter button. They do not perform document detection, auto-cropping, liveness checks, or biometric encryption. Do not ship them in production.
When to Use Stubs
- Early integration — You want to verify the bridge protocol, capability negotiation, and data flow before the SmartCapture SDKs are available or configured.
- CI / automated testing — Stubs work on Simulator (displaying placeholder images) without requiring camera hardware.
- Prototyping — You’re building the host app shell and want a capture flow that “just works” while you focus on other parts of the integration.
StubDocumentCameraView
Presents the rear camera on a real device, or a programmatically drawn placeholder document image on Simulator. Returns a DocumentCaptureResult containing the PNG-encoded image.
import GBGBridge
.fullScreenCover(isPresented: $showDocumentCamera, onDismiss: {
if host.documentCapture.activeRequest != nil {
host.documentCapture.complete(.cancelled(reason: "Dismissed"))
}
}) {
StubDocumentCameraView(
onCaptured: { result in
host.documentCapture.complete(.document(result))
},
onCancelled: {
host.documentCapture.complete(.cancelled(reason: "User dismissed"))
}
)
}
What you get
| Field | Value |
|---|
imageData | PNG-encoded photo from the rear camera (device) or placeholder image (Simulator) |
width / height | Pixel dimensions of the captured image |
mimeType | "image/png" |
What you don’t get
- Document edge detection and auto-crop
- Blur / glare / quality scoring
- Guided capture overlay with real-time feedback
- Auto-capture on frame quality threshold
StubSelfieCameraView
Presents the front camera on a real device, or a programmatically drawn face silhouette on Simulator. Returns a SelfieCaptureResult with the JPEG image and placeholder biometric blobs.
import GBGBridge
.fullScreenCover(isPresented: $showSelfieCamera, onDismiss: {
if host.selfieCapture.activeRequest != nil {
host.selfieCapture.complete(.cancelled(reason: "Dismissed"))
}
}) {
StubSelfieCameraView(
onCaptured: { result in
host.selfieCapture.complete(.selfie(result))
},
onCancelled: {
host.selfieCapture.complete(.cancelled(reason: "User dismissed"))
}
)
}
What you get
| Field | Value |
|---|
previewImageData | JPEG-encoded selfie from the front camera (device) or placeholder image (Simulator) |
width / height | Pixel dimensions of the captured image |
mimeType | "image/jpeg" |
encryptedBlob | Raw JPEG data (placeholder) |
unencryptedBlob | Raw JPEG data (placeholder) |
What you don’t get
- Liveness detection (active or passive)
- Face mesh / landmark analysis
- Encrypted biometric blobs that pass server-side validation
- Guided selfie overlay with positioning feedback
The encryptedBlob and unencryptedBlob fields contain the raw JPEG image data as a stand-in. They are structurally valid (non-empty Data) so the bridge protocol works, but they will not pass liveness verification on the server. This is by design — the stubs exist to prove the integration, not to produce real biometric data.
Simulator Support
On Simulator, there is no camera hardware. Both stub views automatically display programmatically drawn placeholder images — a passport-style document card for StubDocumentCameraView and a face silhouette for StubSelfieCameraView — with a shutter-style capture button. Tapping the shutter generates a synthetic image and returns it through the normal callback, so you can exercise the full capture flow in CI or on your Mac without a physical device.
Swapping Stubs for Real SDKs
The stub views are a drop-in replacement for the real SmartCapture views. The handler setup and bridge wiring stay the same — only the view inside fullScreenCover changes.
Before (stub)
.fullScreenCover(isPresented: $showDocumentCamera) {
StubDocumentCameraView(
onCaptured: { result in
host.documentCapture.complete(.document(result))
},
onCancelled: {
host.documentCapture.complete(.cancelled(reason: "User dismissed"))
}
)
}
After (real SDK)
.fullScreenCover(isPresented: $showDocumentCamera) {
SmartCaptureDocumentView(
documentSide: .front,
onCaptured: { imageData, width, height in
host.documentCapture.complete(.document(
DocumentCaptureResult(imageData: imageData, width: width, height: height)
))
},
onFailed: { message in
host.documentCapture.complete(.failed(
code: "CAPTURE_FAILED", message: message, recoverable: true
))
}
)
}
The key differences when swapping:
- View type — Replace
StubDocumentCameraView / StubSelfieCameraView with your SDK-backed view.
- Callback shape — The real SDK views may have different callback signatures (e.g., separate
onCaptured and onFailed callbacks instead of a single DocumentCaptureResult). Construct the DocumentCaptureResult or SelfieCaptureResult in the callback.
- Nothing else changes — The handler on the typed slot, the
awaitCompletion() / complete() pattern, the bridge protocol, and the fullScreenCover presentation logic all stay the same.
Complete Example
A minimal SwiftUI view using both stub views:
import GBGBridge
import SwiftUI
struct StubJourneyView: View {
@StateObject private var host = BridgeHost(hostVersion: "1.0.0")
@State private var showDocumentCamera = false
@State private var showSelfieCamera = false
let journeyURL: URL
var body: some View {
BridgeWebView(url: journeyURL, host: host)
.onAppear { setupHandlers() }
.onChange(of: host.documentCapture.activeRequest?.correlationId) { _ in
showDocumentCamera = host.documentCapture.activeRequest != nil
}
.onChange(of: host.selfieCapture.activeRequest?.correlationId) { _ in
showSelfieCamera = host.selfieCapture.activeRequest != nil
}
.fullScreenCover(isPresented: $showDocumentCamera, onDismiss: {
if host.documentCapture.activeRequest != nil {
host.documentCapture.complete(.cancelled(reason: "Dismissed"))
}
}) {
StubDocumentCameraView(
onCaptured: { result in
host.documentCapture.complete(.document(result))
},
onCancelled: {
host.documentCapture.complete(.cancelled(reason: "User dismissed"))
}
)
}
.fullScreenCover(isPresented: $showSelfieCamera, onDismiss: {
if host.selfieCapture.activeRequest != nil {
host.selfieCapture.complete(.cancelled(reason: "Dismissed"))
}
}) {
StubSelfieCameraView(
onCaptured: { result in
host.selfieCapture.complete(.selfie(result))
},
onCancelled: {
host.selfieCapture.complete(.cancelled(reason: "User dismissed"))
}
)
}
}
private func setupHandlers() {
let camera = CameraDetector.check()
host.documentCapture.permissionState = camera.permissionState
host.selfieCapture.permissionState = camera.permissionState
host.documentCapture.handler = { [weak host] request in
guard let host else { return .cancelled(reason: "Host deallocated") }
return await host.documentCapture.awaitCompletion()
}
host.selfieCapture.handler = { [weak host] request in
guard let host else { return .cancelled(reason: "Host deallocated") }
return await host.selfieCapture.awaitCompletion()
}
}
}
Next Steps