iOS (Swift)
A complete drop-in Swift client for Volta. No dependencies — just Foundation and URLSession.
VoltaClient
Section titled “VoltaClient”import Foundation
enum VoltaError: LocalizedError { case invalidURL case httpError(Int, String) case noToken
var errorDescription: String? { switch self { case .invalidURL: return "Invalid API URL" case .httpError(let code, let msg): return "HTTP \(code): \(msg)" case .noToken: return "Not authenticated" } }}
struct IndexOption: Identifiable, Decodable { let index: Int let label: String? let image: String? var id: Int { index }}
struct LayoutControl: Identifiable, Decodable { let lid: Int let address: String let type: String let props: [String: AnyCodable] let options: [IndexOption]? var id: Int { lid }
var label: String { props["label"]?.stringValue ?? "\(type) \(lid)" }}
struct LayoutInfo: Decodable { let layoutId: String let activePageId: String let controls: [LayoutControl] let destinationCount: Int}
// Minimal type-erased Decodable for mixed JSON valuesstruct AnyCodable: Decodable { let value: Any var stringValue: String? { value as? String } var boolValue: Bool? { value as? Bool } var doubleValue: Double? { value as? Double }
init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let s = try? container.decode(String.self) { value = s } else if let b = try? container.decode(Bool.self) { value = b } else if let d = try? container.decode(Double.self) { value = d } else if let i = try? container.decode(Int.self) { value = i } else { value = "" } }}
@MainActorclass VoltaClient: ObservableObject { @Published var isConnected = false @Published var layoutInfo: LayoutInfo? @Published var lastError: String?
private var token: String? private let apiURL: String private let apiKey: String private let layoutId: String
init(apiURL: String, apiKey: String, layoutId: String) { self.apiURL = apiURL self.apiKey = apiKey self.layoutId = layoutId }
// MARK: - Connect
func connect() async { lastError = nil do { layoutInfo = try await fetchLayoutInfo() token = try await fetchToken() isConnected = true } catch { lastError = error.localizedDescription isConnected = false } }
// MARK: - Actions
func sendButton(lid: Int) async { await sendAction(lid: lid, type: "Button", value: nil) }
func sendSlider(lid: Int, value: Float) async { await sendAction(lid: lid, type: "Slider", value: .float(value)) }
func sendToggle(lid: Int, value: Bool) async { await sendAction(lid: lid, type: "Toggle", value: .bool(value)) }
func sendIndex(lid: Int, value: Int) async { await sendAction(lid: lid, type: "Index", value: .int(value)) }
// MARK: - Internal
private func fetchLayoutInfo() async throws -> LayoutInfo { let url = try buildURL("/layout/\(layoutId)") var req = URLRequest(url: url) req.setValue(apiKey, forHTTPHeaderField: "x-api-key") let (data, resp) = try await URLSession.shared.data(for: req) guard (resp as? HTTPURLResponse)?.statusCode == 200 else { throw VoltaError.httpError( (resp as? HTTPURLResponse)?.statusCode ?? 0, String(data: data, encoding: .utf8) ?? "" ) } return try JSONDecoder().decode(LayoutInfo.self, from: data) }
private func fetchToken() async throws -> String { let url = try buildURL("/token") var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue(apiKey, forHTTPHeaderField: "x-api-key") req.httpBody = try JSONEncoder().encode(["layoutId": layoutId]) let (data, resp) = try await URLSession.shared.data(for: req) guard (resp as? HTTPURLResponse)?.statusCode == 200 else { throw VoltaError.httpError( (resp as? HTTPURLResponse)?.statusCode ?? 0, String(data: data, encoding: .utf8) ?? "" ) } return try JSONDecoder().decode(TokenResponse.self, from: data).token }
private func sendAction(lid: Int, type: String, value: ActionValue?) async { guard let token else { lastError = "Not connected"; return } do { let url = try buildURL("/action") var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") req.httpBody = try JSONEncoder().encode( ActionRequest(layoutId: layoutId, lid: lid, type: type, value: value) ) let (_, resp) = try await URLSession.shared.data(for: req) guard (resp as? HTTPURLResponse)?.statusCode == 200 else { return } } catch { lastError = error.localizedDescription } }
private func buildURL(_ path: String) throws -> URL { let base = apiURL.hasSuffix("/") ? String(apiURL.dropLast()) : apiURL guard let url = URL(string: base + path) else { throw VoltaError.invalidURL } return url }}
private struct TokenResponse: Decodable { let token: String }private struct ActionRequest: Encodable { let layoutId: String; let lid: Int; let type: String; let value: ActionValue?}private enum ActionValue: Encodable { case float(Float), bool(Bool), int(Int) func encode(to encoder: Encoder) throws { var c = encoder.singleValueContainer() switch self { case .float(let v): try c.encode(v) case .bool(let v): try c.encode(v) case .int(let v): try c.encode(v) } }}let client = VoltaClient( apiURL: "https://your-api-url.execute-api.eu-west-2.amazonaws.com", apiKey: "your-api-key", layoutId: "your-layout-id")
// Connect and discover controlsawait client.connect()
// Send a button pressawait client.sendButton(lid: 1)
// Send a slider valueawait client.sendSlider(lid: 2, value: 0.75)
// Cast a vote (Index control)await client.sendIndex(lid: 3, value: 1)Dynamic UI
Section titled “Dynamic UI”After connecting, client.layoutInfo?.controls contains all controls on the active page. You can dynamically render UI based on control type:
ForEach(client.layoutInfo?.controls ?? []) { control in switch control.type { case "Button": Button(control.label) { Task { await client.sendButton(lid: control.lid) } } case "Slider": Slider(value: $sliderValue, in: 0...1) { _ in Task { await client.sendSlider(lid: control.lid, value: sliderValue) } } case "Index": ForEach(control.options ?? []) { option in Button(option.label ?? "Option \(option.index + 1)") { Task { await client.sendIndex(lid: control.lid, value: option.index) } } } default: Text("\(control.label) — unsupported type") }}Demo app
Section titled “Demo app”A complete SwiftUI demo app is available at demos/ios/VoltaDemo/ in the repository. It dynamically discovers controls and renders appropriate UI for each type, including image grids for sticker-style Index controls.