Mobile SDKs

Swift (iOS)

VocallSDK -- async/await, Combine, SwiftUI

Swift SDK

The Swift SDK provides a native client for integrating Vocall into iOS and macOS applications. It uses URLSessionWebSocketTask for transport and @Published properties with Combine for reactive state management. No external dependencies are required.

Installation

Xcode: File > Add Package Dependencies, then enter the repository URL:

https://github.com/primoia/vocall-swift-sdk.git

Package.swift:

dependencies: [
    .package(url: "https://github.com/primoia/vocall-swift-sdk.git", from: "0.1.0")
]

Requirements

| Requirement | Minimum Version | |-------------|-----------------| | Swift | 5.9+ | | iOS | 16.0+ | | macOS | 13.0+ | | Dependencies | None |

Creating a Client

VocallClient is marked @MainActor and conforms to ObservableObject, making it ready for SwiftUI out of the box:

let client = VocallClient(
    serverURL: URL(string: "wss://engine.example.com")!,
    token: "your-auth-token",
    visitorId: "user-abc-123"
)

Published Properties

Observe state changes reactively in SwiftUI or via Combine:

@Published var status: VocallStatus        // Current connection/processing state
@Published var messages: [ChatMessage]      // Chat history
@Published var sessionId: String?           // Active session identifier
@Published var isConnected: Bool            // WebSocket connection state
@Published var pendingConfirmSeq: Int?      // Awaiting user confirmation
@Published var voiceEnabled: Bool           // Whether voice is available

VocallStatus Enum

enum VocallStatus: String, Codable {
    case idle
    case connecting
    case connected
    case ready
    case processing
    case disconnected
    case error
}

Use a switch to handle each state:

switch client.status {
case .idle:
    Text("Not connected")
case .connecting:
    ProgressView("Connecting...")
case .connected, .ready:
    Text("Ready").foregroundColor(.green)
case .processing:
    ProgressView("Thinking...")
case .disconnected:
    Text("Disconnected").foregroundColor(.gray)
case .error:
    Text("Error").foregroundColor(.red)
}

Callbacks

Register closures for engine-driven events:

client.onToast = { message, level in
    toastManager.show(message, level: level)
}

client.onConfirm = { seq, message in
    showConfirmAlert(seq: seq, message: message)
}

client.onCommand = { command in
    handleCommand(command)
}

client.onStatusChange = { newStatus in
    print("Status changed to: \(newStatus)")
}

client.onChatMessage = { message in
    print("New message: \(message.text)")
}

Methods

// Connect with a manifest (async)
try await client.connect(manifest: manifest)

// Send a text message
client.sendText("Emitir nota fiscal para o tomador Maria")

// Respond to a confirmation prompt
client.sendConfirm(seq: 1, confirmed: true)

// Send a command result back to the engine
client.sendResult(seq: 2, success: true, data: ["id": "456"])

// Close the connection
client.disconnect()

Field Registry

Bind your UI elements so the engine can read values and trigger actions:

// Register a text field
client.fieldRegistry.registerField(
    screenId: "emitir_nfse",
    fieldId: "valor_servico",
    getValue: { self.valorServico },
    setValue: { value in self.valorServico = value }
)

// Register an action
client.fieldRegistry.registerAction(
    screenId: "emitir_nfse",
    actionId: "submit",
    handler: { self.submitForm() }
)

// Navigation
client.fieldRegistry.onNavigate = { screenId in
    self.selectedTab = screenId
}

// Modal management
client.fieldRegistry.onOpenModal = { modalId, data in
    self.presentModal(id: modalId, data: data)
}
client.fieldRegistry.onCloseModal = { modalId in
    self.dismissModal(id: modalId)
}

SwiftUI Integration

Use @StateObject to own the client in your view hierarchy:

struct ContentView: View {
    @StateObject private var vocall = VocallClient(
        serverURL: URL(string: "wss://engine.example.com")!,
        token: "tok_abc",
        visitorId: "visitor-1"
    )

    var body: some View {
        VStack {
            StatusBadge(status: vocall.status)

            ScrollView {
                ForEach(vocall.messages) { msg in
                    MessageBubble(message: msg)
                }
            }

            HStack {
                TextField("Type a message...", text: $inputText)
                Button("Send") {
                    vocall.sendText(inputText)
                    inputText = ""
                }
            }
        }
        .task {
            try? await vocall.connect(manifest: buildManifest())
        }
    }

    @State private var inputText = ""
}