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