Realtime API - Swift SDK Documentation
Overview
The Realtime API enables real-time updates for collection records using Server-Sent Events (SSE). It allows you to subscribe to changes in collections or specific records and receive instant notifications when records are created, updated, or deleted.
Key Features:
- Real-time notifications for record changes
- Collection-level and record-level subscriptions
- Automatic connection management and reconnection
- Authorization support
- Subscription options (expand, custom headers, query params)
- Event-driven architecture
📖 Reference: This guide mirrors the JavaScript SDK Realtime documentation but uses Swift syntax and examples.
Backend Endpoints:
GET /api/realtime- Establish SSE connectionPOST /api/realtime- Set subscriptions
How It Works
- Connection: The SDK establishes an SSE connection to
/api/realtime - Client ID: Server sends
PB_CONNECTevent with a uniqueclientId - Subscriptions: Client submits subscription topics via POST request
- Events: Server sends events when matching records change
- Reconnection: SDK automatically reconnects on connection loss
Basic Usage
Subscribe to Collection Changes
Subscribe to all changes in a collection:
import BosBase
let client = try BosBaseClient(baseURLString: "http://127.0.0.1:8090")
// Subscribe to all changes in the 'posts' collection
let unsubscribe = try await client
.collection("posts")
.subscribe("*") { event in
print("Action: \(event.action)") // 'create', 'update', or 'delete'
print("Record: \(event.record)") // The record data
}
// Later, unsubscribe
await unsubscribe()
Subscribe to Specific Record
Subscribe to changes for a single record:
// Subscribe to changes for a specific post
try await client
.collection("posts")
.subscribe("RECORD_ID") { event in
print("Record changed: \(event.record)")
print("Action: \(event.action ?? "")")
}
Multiple Subscriptions
You can subscribe multiple times to the same or different topics:
// Subscribe to multiple records
let unsubscribe1 = try await client
.collection("posts")
.subscribe("RECORD_ID_1") { event in
print("Change event: \(event)")
}
let unsubscribe2 = try await client
.collection("posts")
.subscribe("RECORD_ID_2") { event in
print("Change event: \(event)")
}
let unsubscribe3 = try await client
.collection("posts")
.subscribe("*") { event in
print("Collection-wide change: \(event)")
}
// Unsubscribe individually
await unsubscribe1()
await unsubscribe2()
await unsubscribe3()
Subscription Options
With Expand
Subscribe with related records expanded:
let options = RecordSubscriptionOptions(expand: "author,comments")
let unsubscribe = try await client
.collection("posts")
.subscribe("RECORD_ID", options: options) { event in
if let author = (event.record["expand"] as? [String: AnyCodable])?["author"] {
print("Author: \(author)")
}
}
With Filter
Subscribe only to records matching a filter:
let options = RecordSubscriptionOptions(
filter: client.filter("status = {:status}", params: ["status": "published"])
)
let unsubscribe = try await client
.collection("posts")
.subscribe("*", options: options) { event in
// Only receives events for published posts
print("Published post changed: \(event.record)")
}
With Custom Headers
let options = RecordSubscriptionOptions(
headers: ["X-Custom-Header": "value"]
)
let unsubscribe = try await client
.collection("posts")
.subscribe("*", options: options) { event in
// Subscription with custom headers
}
Event Types
Record Events
try await client
.collection("posts")
.subscribe("*") { event in
switch event.action {
case "create":
print("New post created: \(event.record)")
case "update":
print("Post updated: \(event.record)")
case "delete":
print("Post deleted: \(event.record)")
default:
print("Unknown action: \(event.action ?? "")")
}
}
Connection Events
The realtime service provides connection status:
// Check if connected
let clientId = await client.realtime.currentClientIdentifier()
if clientId != nil {
print("Connected with client ID: \(clientId!)")
}
// Handle disconnection
client.realtime.onDisconnect = { activeSubscriptions in
print("Disconnected. Active subscriptions: \(activeSubscriptions)")
}
Unsubscribing
Unsubscribe from Specific Topic
// Unsubscribe from a specific record
try await client
.collection("posts")
.unsubscribe("RECORD_ID")
// Unsubscribe from all collection subscriptions
try await client
.collection("posts")
.unsubscribe()
Unsubscribe by Prefix
// Unsubscribe from all topics starting with a prefix
try await client.realtime.unsubscribeByPrefix("posts/")
Complete Example
import BosBase
class PostManager {
let client: BosBaseClient
private var subscriptions: [() -> Void] = []
init() throws {
client = try BosBaseClient(baseURLString: "http://localhost:8090")
}
func startListening() async throws {
// Authenticate first
try await client
.collection("users")
.authWithPassword(identity: "user@example.com", password: "password123")
// Subscribe to all posts
let unsubscribeAll = try await client
.collection("posts")
.subscribe("*") { [weak self] event in
self?.handlePostEvent(event)
}
subscriptions.append(unsubscribeAll)
// Subscribe to specific post
let unsubscribeSpecific = try await client
.collection("posts")
.subscribe("POST_ID") { [weak self] event in
self?.handlePostEvent(event)
}
subscriptions.append(unsubscribeSpecific)
// Subscribe with filter
let options = RecordSubscriptionOptions(
filter: client.filter("status = {:status}", params: ["status": "published"])
)
let unsubscribeFiltered = try await client
.collection("posts")
.subscribe("*", options: options) { [weak self] event in
self?.handlePostEvent(event)
}
subscriptions.append(unsubscribeFiltered)
}
private func handlePostEvent(_ event: RecordSubscription<JSONRecord>) {
switch event.action {
case "create":
print("New post: \(event.record["title"] ?? "")")
case "update":
print("Updated post: \(event.record["title"] ?? "")")
case "delete":
print("Deleted post: \(event.record["id"] ?? "")")
default:
break
}
}
func stopListening() async {
for unsubscribe in subscriptions {
await unsubscribe()
}
subscriptions.removeAll()
}
}
// Usage
let manager = try PostManager()
try await manager.startListening()
// Later, stop listening
await manager.stopListening()
SwiftUI Integration Example
import SwiftUI
import BosBase
class RealtimeViewModel: ObservableObject {
@Published var posts: [JSONRecord] = []
private let client: BosBaseClient
private var unsubscribe: (() -> Void)?
init() throws {
client = try BosBaseClient(baseURLString: "http://localhost:8090")
}
func startListening() async throws {
// Load initial posts
let result: ListResult<JSONRecord> = try await client
.collection("posts")
.getList()
await MainActor.run {
self.posts = result.items
}
// Subscribe to changes
unsubscribe = try await client
.collection("posts")
.subscribe("*") { [weak self] event in
Task { @MainActor in
guard let self = self else { return }
switch event.action {
case "create":
self.posts.append(event.record)
case "update":
if let index = self.posts.firstIndex(where: {
($0["id"]?.value as? String) == (event.record["id"]?.value as? String)
}) {
self.posts[index] = event.record
}
case "delete":
self.posts.removeAll {
($0["id"]?.value as? String) == (event.record["id"]?.value as? String)
}
default:
break
}
}
}
}
func stopListening() async {
await unsubscribe?()
unsubscribe = nil
}
}
struct PostsView: View {
@StateObject private var viewModel: RealtimeViewModel
init() throws {
_viewModel = StateObject(wrappedValue: try RealtimeViewModel())
}
var body: some View {
List(viewModel.posts, id: \.self) { post in
Text(post["title"]?.value as? String ?? "")
}
.task {
try? await viewModel.startListening()
}
.onDisappear {
Task {
await viewModel.stopListening()
}
}
}
}
Error Handling
do {
let unsubscribe = try await client
.collection("posts")
.subscribe("*") { event in
print("Event: \(event)")
}
// Store unsubscribe function for later
// ...
} catch let error as ClientResponseError {
print("Realtime error: \(error.status) - \(error.response ?? [:])")
} catch {
print("Unexpected error: \(error)")
}
Best Practices
- Authenticate First: Always authenticate before subscribing to protected collections
- Unsubscribe Properly: Always call the unsubscribe function when done to clean up resources
- Handle Disconnections: Implement
onDisconnecthandler to notify users of connection issues - Filter Subscriptions: Use filters to reduce unnecessary events
- Memory Management: Store unsubscribe functions and call them in
deinitor cleanup methods - Thread Safety: Realtime callbacks may be called on background threads; use
@MainActorfor UI updates