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.
This example demonstrates bidirectional messaging between the native iOS host and the web journey: sending events to the web journey and handling incoming requests with capability handlers.
What this example demonstrates
Sending events from native to web
Declaring capture support via typed capability slots
Registering custom capabilities for non-camera actions
Responding with success, error, and cancellation statuses
Observing all bridge traffic via BridgeHostDelegate
Complete source
import GBGBridge
import SwiftUI
// MARK: - App Entry Point
@main
struct TwoWayApp : App {
var body: some Scene {
WindowGroup {
TwoWayJourneyView ()
}
}
}
// MARK: - Main View
struct TwoWayJourneyView : View {
@StateObject private var host = BridgeHost ( hostVersion : "1.0.0" )
@StateObject private var logger = MessageLogger ()
private let journeyURL = URL ( string : "https://journey.example.com" ) !
var body: some View {
NavigationStack {
VStack ( spacing : 0 ) {
// Journey WebView
BridgeWebView ( url : journeyURL, host : host)
// Message log (bottom panel)
MessageLogView ( messages : logger. log )
. frame ( height : 200 )
}
. navigationTitle ( "Two-Way Bridge" )
. navigationBarTitleDisplayMode (. inline )
. toolbar {
ToolbarItem ( placement : . navigationBarTrailing ) {
Button ( "Send Ping" ) {
sendPingEvent ()
}
}
}
. onAppear {
setupBridge ()
}
}
}
private func setupBridge () {
// Declare document capture support via typed slot
host. documentCapture . handler = { [ weak host] request in
guard let host else { return . cancelled ( reason : "Host deallocated" ) }
// Simulate a capture delay
try ? await Task. sleep ( nanoseconds : 1_000_000_000 )
// Return a mock result
let mockData = Data ( repeating : 0xFF , count : 100 )
return . document ( DocumentCaptureResult (
imageData : mockData,
width : 1920 ,
height : 1080 ,
mimeType : "image/jpeg"
))
}
// Register custom capability for device info
host. registerCustomCapability ( "device.info" , version : "1.0" ) { request, responder in
let info: [ String : JSONValue] = await MainActor. run {
let device = UIDevice. current
return [
"model" : . string (device. model ),
"systemName" : . string (device. systemName ),
"systemVersion" : . string (device. systemVersion ),
"name" : . string (device. name ),
"userInterfaceIdiom" : . string (device. userInterfaceIdiom == . pad ? "pad" : "phone" )
]
}
responder. respond ( status : . success , data : info, error : nil )
}
// Set delegate for logging
host. delegate = logger
// Send initial event
host. send ( event : "host.ready" , data : [
"timestamp" : . number ( Date (). timeIntervalSince1970 * 1000 ),
"version" : . string ( "1.0.0" )
])
}
private func sendPingEvent () {
host. send ( event : "host.ping" , data : [
"timestamp" : . number ( Date (). timeIntervalSince1970 * 1000 )
])
}
}
// MARK: - Message Logger (Delegate)
/// Observes all bridge traffic and maintains a log for the debug UI.
@MainActor
final class MessageLogger : ObservableObject , BridgeHostDelegate {
struct LogEntry : Identifiable {
let id = UUID ()
let timestamp: Date
let direction: String
let type: String
let action: String
}
@Published var log: [LogEntry] = []
func bridgeHost ( _ host : BridgeHost, didReceive message : BridgeMessage) {
log. append ( LogEntry (
timestamp : Date (),
direction : "IN" ,
type : message. type . rawValue ,
action : message. payload . action
))
}
func bridgeHost ( _ host : BridgeHost, unhandledRequest message : BridgeMessage) {
log. append ( LogEntry (
timestamp : Date (),
direction : "IN" ,
type : "UNHANDLED" ,
action : message. payload . action
))
// Respond with unsupported for unknown actions
host. respond (
to : message. correlationId ,
status : . unsupported ,
error : BridgeErrorPayload (
code : "UNSUPPORTED" ,
message : "Action ' \( message. payload . action ) ' is not supported" ,
recoverable : false
)
)
}
}
// MARK: - Message Log View
struct MessageLogView : View {
let messages: [MessageLogger.LogEntry]
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter ()
formatter. dateFormat = "HH:mm:ss.SSS"
return formatter
}()
var body: some View {
VStack ( alignment : . leading , spacing : 0 ) {
HStack {
Text ( "Bridge Log" )
. font (. caption )
. fontWeight (. semibold )
Spacer ()
Text ( " \( messages. count ) messages" )
. font (. caption2 )
. foregroundColor (. secondary )
}
. padding (. horizontal , 12 )
. padding (. vertical , 6 )
. background ( Color (. systemGroupedBackground ))
if messages. isEmpty {
Spacer ()
HStack {
Spacer ()
Text ( "No messages yet" )
. font (. caption )
. foregroundColor (. secondary )
Spacer ()
}
Spacer ()
} else {
ScrollViewReader { proxy in
List (messages) { entry in
HStack ( spacing : 8 ) {
Text (dateFormatter. string ( from : entry. timestamp ))
. font (. system (. caption2 , design : . monospaced ))
. foregroundColor (. secondary )
Text (entry. direction )
. font (. system (. caption2 , design : . monospaced ))
. fontWeight (. bold )
. foregroundColor (entry. direction == "IN" ? . blue : . green )
Text (entry. type )
. font (. system (. caption2 , design : . monospaced ))
. foregroundColor (. orange )
Text (entry. action )
. font (. system (. caption2 , design : . monospaced ))
. lineLimit ( 1 )
}
. listRowInsets ( EdgeInsets ( top : 2 , leading : 12 , bottom : 2 , trailing : 12 ))
}
. listStyle (. plain )
. onChange ( of : messages. count ) { _ in
if let last = messages. last {
proxy. scrollTo (last. id , anchor : . bottom )
}
}
}
}
}
. background ( Color (. systemBackground ))
. overlay (
Rectangle ()
. frame ( height : 1 )
. foregroundColor ( Color (. separator )),
alignment : . top
)
}
}
See all 219 lines
Code explanation
The sections below cover what each part of the app does, in roughly the order theyβre exercised at runtime.
Outgoing events (native β web)
// Send an event when the host is ready
host. send ( event : "host.ready" , data : [
"timestamp" : . number ( Date (). timeIntervalSince1970 * 1000 ),
"version" : . string ( "1.0.0" )
])
The web journey receives this via window.GBGBridge.receive() and can use the timestamp and version information to initialize its state.
Incoming requests (web β native)
When the web journey sends a camera.document.capture request, the typed slot handler runs:
Web Journey BridgeHost documentCapture slot
β β β
βββ request: camera.document ββΊβ β
β .capture βββ handler closure ββββββΊβ
β β β (simulate capture)
β ββββ CaptureResult ββββββββ€
β β (auto-encoded) β
ββββ response: success βββββββββ€ β
β { imageBase64, imageWidth, β β
β imageHeight, mimeType } β β
The CaptureResult is automatically encoded to the bridge protocol format β no manual JSONValue dictionary construction needed.
Unhandled Requests
The MessageLogger delegate catches requests with no registered handler and responds with .unsupported. This prevents the web journey from waiting indefinitely for a response.
Running This Example
Add GBGBridge to your project via SPM.
Create a new SwiftUI App target.
Replace the default ContentView with the code above.
Update journeyURL to point to your web journey.
Build and run.
Next Steps