This tutorial walks you through building a complete iOS app that runs a GBG Go identity verification journey. By the end, you will have a working SwiftUI app that launches a web-based journey, handles document and selfie capture requests from the journey, and returns results back to the web via the native bridge. The app you build has three screens: a configuration form, a WebView running the journey, and full-screen camera views that appear when the journey requests a capture. The entire integration takes about 100 lines of Swift.Documentation Index
Fetch the complete documentation index at: https://docs.go.gbgplc.com/llms.txt
Use this file to discover all available pages before exploring further.
Reference app: The complete source code for this tutorial is available at gbg-go-ios-reference. You can clone it and run it immediately, or follow this tutorial to build it step by step.
Prerequisites
| Requirement | Version | Notes |
|---|---|---|
| Xcode | 15.0+ | Swift 5.9 toolchain |
| iOS target | 16.0+ | NavigationStack, onChange(of:) |
| Node.js | 18+ | For the companion server |
| GBG Go credentials | — | Client ID, secret, username, password. Contact your GBG account representative. |
Set Up the Backend
The iOS app does not call the GBG Go API directly. Instead, it calls a lightweight companion server that creates journey sessions using the GBG Go Core SDK. This keeps your API credentials on the server, never on the device.1. Clone the reference repository
2. Install dependencies
3. Configure credentials
Copy the example environment file and fill in your GBG Go credentials:.env in a text editor:
4. Start the server
5. Test the server
Open a new terminal and verify it responds:{"ok":true}
Leave the server running. You will need it for the rest of the tutorial.
Create Your iOS Project
Open Xcode and create a new project:- File → New → Project
- Choose App under the iOS tab.
- Set the product name to GBGGoReference.
- Set the interface to SwiftUI and the language to Swift.
- Click Create.
Add GBGBridge via Swift Package Manager
- File → Add Package Dependencies…
- Enter the GBGBridge package URL.
- Select Up to Next Major Version from
1.0.0. - Click Add Package and ensure GBGBridge is added to your app target.
Configure Info.plist
Add these keys to yourInfo.plist:
NSAllowsLocalNetworking permits HTTP requests to localhost during development. Without it, requests to the companion server silently fail.
File structure
Create the following group structure in Xcode’s Project Navigator:App Entry Point
Replace the contents ofGBGGoReferenceApp.swift with:
JourneyView.
Navigation Setup
CreateRootView.swift in the App group:
RootView manages a NavigationStack with two destinations. When the companion server returns a journey URL, the closure pushes it onto the navigation path, which triggers the JourneyView.
Configuration Screen
CreateSetupView.swift in the Settings group:
- Server URL — the companion server address. Defaults to
http://localhost:3000, which works in the Simulator. On a physical device, use your Mac’s local IP address (e.g.http://192.168.1.42:3000). - Resource ID — identifies which journey template to run. Leave blank to use the server’s default.
@AppStorage.
Calling Your Backend
CreateJourneyService.swift in the Journey group:
JourneyService makes a single POST request to the companion server’s /api/journey/start endpoint. The server authenticates with GBG Go, creates a journey session, registers a mobile device, and returns a journey URL. The iOS app never touches API credentials directly.
The StartResponse includes:
| Field | Purpose |
|---|---|
journeyUrl | The URL to load in BridgeWebView |
instanceId | Unique journey session identifier |
connectToken | Short-lived device authentication token |
expiresIn | Token validity in seconds (~120s) |
The connect token expires quickly. If the journey URL fails to load, go back and tap “Start Journey” again to get a fresh token.
The Bridge Integration
This is the core of the integration. CreateJourneyView.swift in the Journey group:
How the Bridge Works
TheBridgeHost is the native side of a bidirectional communication channel between your Swift code and the JavaScript running inside the WebView. When the web journey needs a native capability — like capturing a document photo — it sends a request message through the bridge. Your Swift code handles the request and sends a response back.
BridgeWebView wraps WKWebView with the bridge pre-configured. It injects a bootstrap script at document start, registers a JavaScript message handler, and attaches the host. You provide the URL and the host — everything else is automatic.
The Typed Slot Pattern
BridgeHost exposes two built-in properties called typed slots:
host.documentCapture— handles document photo capture requestshost.selfieCapture— handles selfie/liveness capture requests
CaptureCapability object with a handler property. Setting a handler does two things:
- It defines what happens when the journey requests that capture type.
- It automatically marks the capability as “supported” in
capability.queryresponses, so the journey knows the app can handle it.
- The web journey sends a capture request through the bridge.
- The slot’s handler closure is called.
- The handler calls
awaitCompletion(), which suspends — the closure pauses and waits. - The
activeRequestproperty becomes non-nil, which triggersonChangeto setshowDocumentCamera = true. - SwiftUI presents the camera view via
fullScreenCover. - The user captures a photo (or cancels).
- The camera view calls
slot.complete(.document(result))orslot.cancelIfBusy(). awaitCompletion()resumes, returning the result.- The handler returns the result, which the bridge sends back to the journey.
activeRequestbecomes nil, dismissing the camera.
Why [weak host]?
The handler closures capture [weak host] to prevent a retain cycle. Without it: the closure retains the host, the host retains the WebView’s configuration, and the configuration retains the closure — a cycle that leaks memory.
Why onAppear and not the initializer?
Handlers are assigned in onAppear, not in init. The @StateObject host persists across SwiftUI re-renders, but onAppear is the idiomatic place for setup that should happen once when the view appears.
Why onChange with correlationId?
SwiftUI’s onChange modifier needs an Equatable value to observe. BridgeMessage itself is not Equatable, but correlationId (a String) is. When a new capture request arrives, the correlation ID changes, triggering the closure. When the request completes, activeRequest becomes nil, and the closure sets the presentation boolean to false.
iOS 16 uses
onChange(of:) { newValue in ... }. iOS 17+ uses onChange(of:) { ... } without a parameter. The code above targets iOS 16.Handling Capture Requests
TheStubDocumentCameraView and StubSelfieCameraView are included in the GBGBridge SDK. They provide a working capture UI out of the box:
- On a real device: They present
UIImagePickerControllerwith the rear camera (document) or front camera (selfie). - On the Simulator: They display programmatically drawn placeholder images — a passport-style document card or a face silhouette — with a shutter button. This is expected behaviour, not a bug. The Simulator has no camera hardware.
| Callback | Called when |
|---|---|
onCaptured | The user captures a photo. Receives a DocumentCaptureResult or SelfieCaptureResult. |
onCancelled | The user dismisses the camera without capturing. |
onCaptured callback, call slot.complete() with the result. In onCancelled, call slot.cancelIfBusy(). The bridge sends the appropriate response back to the journey.
Run the App
- Make sure the companion server is running. Run
node index.mjsin theserver/directory. - Select an iPhone Simulator in Xcode (e.g. iPhone 15 Pro).
- Press Cmd+R to build and run.
- On the Setup screen, leave the server URL as
http://localhost:3000. - Tap Start Journey.
- The journey loads in the WebView. When it requests a document capture, the stub camera appears as a full-screen overlay.
- Tap the shutter button to capture. The result is sent back to the journey.
Common Pitfalls
A few rough edges trip up most teams running the tutorial app for the first time — Simulator-vs-device URLs, ATS rejectinghttp:// in production, and SwiftUI state-ownership patterns. The notes below explain how to avoid them.
Simulator vs device
http://localhost:3000 only works in the Simulator. On a physical device, the companion server is not on localhost — use your Mac’s IP address instead (e.g. http://192.168.1.42:3000). Find your IP with:
App Transport Security
If you forgetNSAllowsLocalNetworking in Info.plist, HTTP requests to the companion server fail silently. There is no error in the console — the request simply never reaches the server. Always check this first if the Setup screen shows a network error.
Camera on Simulator
The Simulator has no camera. The stub views automatically display programmatically drawn placeholder images with a shutter button. This is expected behaviour.Connect token expiry
The connect token from the server is short-lived (~120 seconds). If the journey URL does not load in time, go back and tap “Start Journey” again for a fresh token.Handler assignment timing
Assign handlers inonAppear, not in the initializer. The @StateObject host persists across re-renders, but onAppear is the SwiftUI-idiomatic place for one-time setup.
Memory leak prevention
Use[weak host] in handler closures. Without it, the closure captures a strong reference to the host, which captures the WebView, creating a retain cycle that leaks memory.
What’s Next
- Tutorial Part 2: Integrate Smart Capture SDKs — Replace stub camera views with production document scanning and face capture with liveness detection.
- API Reference — Full documentation for
BridgeHost,BridgeWebView,CaptureCapability, and all message types. - Concepts — Deeper dive into the bridge architecture, message protocol, and capability system.
- NFC Reading — Add passport chip reading as a custom capability.
- UIKit Integration — Use
BridgeWebViewConfiguratordirectly withUIViewControllerinstead of SwiftUI.