Swift
Use SockudoSwift on iOS, macOS, tvOS, watchOS, and visionOS.
SockudoSwift is the official realtime client for Apple platforms. It supports public, private, presence, encrypted channels, auth endpoints, V2 recovery, rewind, filters, deltas, mutable messages, and proxy-backed history helpers.
Supported deployment targets are iOS 13+, macOS 10.15+, tvOS 13+, watchOS
6+, and visionOS 1+. The package uses Swift 6.2 concurrency and isolates the
client, channels, and callbacks on @SockudoActor.
Install
Install through Swift Package Manager from the SockudoSwift mirror. SwiftPM resolves this from
the v2.1.0 Git tag and the mirror repository's root Package.swift manifest:
.package(url: "https://github.com/sockudo/sockudo-swift", from: "2.1.0").target(
name: "YourApp",
dependencies: [
.product(name: "SockudoSwift", package: "sockudo-swift"),
]
)Connect
import SockudoSwift
let client = try SockudoClient(
"app-key",
options: .init(
cluster: "local",
forceTLS: false,
enabledTransports: [.ws],
wsHost: "127.0.0.1",
wsPort: 6001,
wssPort: 6001,
protocolVersion: 2,
connectionRecovery: true
)
)
let channel = client.subscribe("public-updates")
channel.bind("price-updated") { data, _ in
print(data ?? "")
}
client.connect()Protocol V1 compatibility is the default. Set protocolVersion: 2 explicitly
for new Sockudo applications that need continuity metadata and V2 features.
Use the public ingress hostname with forceTLS: true in production.
Concurrency and lifecycle
From code outside @SockudoActor, access client methods with await:
func startRealtime() async {
await client.connect()
}
func stopRealtime() async {
await client.unsubscribe("public-updates")
await client.disconnect()
}Event callbacks execute on the dedicated Sockudo actor, not the main thread.
Hop to MainActor for UIKit or SwiftUI state:
channel.bind("price-updated") { data, _ in
Task { @MainActor in
viewModel.latestPrice = String(describing: data)
}
}Keep the EventBindingToken returned by bind when you want to remove one
handler with unbind(eventName:token:); call unbindAll() only when the
channel owner is being torn down.
Unexpected disconnects reconnect with bounded exponential backoff. Tune
maxReconnectAttempts and maxReconnectGapInSeconds for the app's foreground
and background policy. Client events emitted while disconnected are buffered
up to 50 per channel and replayed after subscription; unsubscribing clears that
buffer.
Auth
let client = try SockudoClient(
"app-key",
options: .init(
cluster: "local",
forceTLS: false,
wsHost: "127.0.0.1",
wsPort: 6001,
channelAuthorization: .init(
endpoint: "https://api.example.com/sockudo/auth"
)
)
)The backend must authenticate the current user and authorize the exact
channel_name before signing. Presence identity must come from the server-side
session, not a value supplied by the app.
Protocol V2 also supports scoped capability tokens:
let client = try SockudoClient(
"app-key",
options: .init(
cluster: "local",
protocolVersion: 2,
forceTLS: true,
wsHost: "realtime.example.com",
capabilityToken: .init(asyncProvider: {
try await tokenService.fetchSockudoToken()
})
)
)JWTs with expiry metadata refresh proactively. Opaque tokens refresh when the server reports expiration, provided an async provider is configured.
Presence
Subscribe as PresenceChannel to access members and V2 in-place presence
updates:
let presence = client.subscribe("presence-agent:session-123") as! PresenceChannel
presence.bind("sockudo:member_added") { member, _ in
print(member as Any)
}
presence.bind("sockudo:presence_update") { member, _ in
print(member as Any)
}
try presence.update(data: ["status": "thinking"])Wait for the subscription-success event before treating the initial membership set as authoritative.
Filters and deltas
let channel = client.subscribe(
"price:btc",
options: .init(
filter: .eq("market", "spot"),
delta: .init(enabled: true, algorithm: .xdelta3),
events: ["price.updated"],
expression: .source("data.price >= `100`")
)
)Recovery and rewind
let channel = client.subscribe(
"market:BTC",
options: .init(rewind: .seconds(30))
)
channel.bind("message") { _, _ in
print(client.recoveryPosition(for: "market:BTC") as Any)
}
client.bind("sockudo:resume_success") { data, _ in
print(data as Any)
}Mutable messages
var state: MutableMessageState? = nil
let channel = client.subscribe("chat:room-1")
channel.bindGlobal { _, data in
guard
let event = data as? SockudoEvent,
isMutableMessageEvent(event)
else { return }
state = try? reduceMutableMessageEvent(current: state, event: event)
}Presence history proxy
let client = try SockudoClient(
"app-key",
options: .init(
cluster: "local",
forceTLS: false,
wsHost: "127.0.0.1",
wsPort: 6001,
presenceHistory: .init(
endpoint: "https://api.example.com/sockudo/presence-history"
)
)
)
let channel = client.subscribe("presence-lobby") as! PresenceChannel
channel.history(.init(limit: 50, direction: "newest_first")) { result in
print(result)
}History endpoints are backend proxies because a mobile client must not sign Sockudo REST requests. Authorize the caller and channel on every request, bound page sizes, and forward opaque cursors unchanged.
Encrypted channels
private-encrypted-* channels decrypt automatically when the protected-channel
auth response contains a derived shared_secret:
let encrypted = client.subscribe("private-encrypted-documents")
encrypted.bind("doc-updated") { payload, _ in
print(payload as Any)
}Keep the encryption master key on the backend. Continue to use TLS and normal channel authorization.
Push notifications on Apple platforms
Use APNs to obtain a device token, then send it to your backend. Your backend registers the device with Sockudo and keeps APNs credentials server-side.
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
Task {
await registerDeviceWithBackend(
deviceId: UIDevice.current.identifierForVendor?.uuidString ?? token,
platform: "apns",
providerToken: token
)
}
}Do not embed APNs private keys or Sockudo app secrets in the app.
Production checklist
- Use one long-lived client for the app session and disconnect it on explicit sign-out.
- Select Protocol V2 deliberately; do not assume the default changed during a Pusher migration.
- Move UI mutations from
@SockudoActortoMainActor. - Apply an app-specific reconnect limit for backgrounded mobile processes.
- Treat failed recovery as a signal to reload authoritative application state.
- Keep auth, history, versioned-message, and push proxy credentials on the backend.
- Unbind view-owned callbacks to prevent retained views and duplicate updates.
- Test offline/online transitions, token expiry, app suspension, and a rolling Sockudo restart.