> ## 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.

# Two-Way Communication

> Bidirectional messaging between your app and the web journey.

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

```swift expandable theme={null}
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
        )
    }
}
```

### 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)

```swift theme={null}
// 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

1. Add GBGBridge to your project via SPM.
2. Create a new SwiftUI App target.
3. Replace the default `ContentView` with the code above.
4. Update `journeyURL` to point to your web journey.
5. Build and run.

## Next Steps

* [Advanced Integration](/docs/go-v2/developer-integration/sdks/ios/examples/advanced-integration) — Lifecycle management, custom configuration, and capability checks
* [Messaging Guide](/docs/go-v2/developer-integration/sdks/ios/messaging) — Deep dive on message patterns
