Installation
Add InsForge to your Swift Package Manager dependencies:
dependencies: [
.package(url: "https://github.com/insforge/insforge-swift.git", from: "0.0.7")
]
import InsForge
let insforge = InsForgeClient(
baseURL: URL(string: "https://your-app.insforge.app")!,
anonKey: "your-anon-key"
)
Enable Logging (Optional)
For debugging, you can configure the SDK log level and destination:
let options = InsForgeClientOptions(
global: .init(
logLevel: .debug,
logDestination: .osLog,
logSubsystem: "com.example.MyApp"
)
)
let insforge = InsForgeClient(
baseURL: URL(string: "https://your-app.insforge.app")!,
anonKey: "your-anon-key",
options: options
)
Log Levels:
| Level | Description |
|---|
.trace | Most verbose, includes all internal details |
.debug | Detailed information for debugging |
.info | General operational information (default) |
.warning | Warnings that don’t prevent operation |
.error | Errors that affect functionality |
.critical | Critical failures |
Log Destinations:
| Destination | Description |
|---|
.console | Standard output (print) |
.osLog | Apple’s unified logging system (recommended for iOS/macOS) |
.none | Disable logging |
.custom | Provide your own LogHandler factory |
Use .info or .error in production to avoid exposing sensitive data in logs.
connect()
Establish a WebSocket connection to the realtime server.
Example
do {
try await insforge.realtime.connect()
print("Connected to realtime server")
} catch {
print("Connection failed: \(error)")
}
subscribe()
Subscribe to a channel to receive messages.
Parameters
to (String) - Channel name (e.g., “orders:123”, “chat:room-1”)
onMessage (Closure) - Callback for receiving messages
Example
await insforge.realtime.subscribe(to: "chat:lobby") { message in
print("Event: \(message.eventName ?? "")")
print("Channel: \(message.channelName ?? "")")
if let payload = message.payload {
print("Data: \(payload)")
}
if let senderId = message.senderId {
print("From: \(senderId)")
}
}
unsubscribe()
Unsubscribe from a channel.
Example
await insforge.realtime.unsubscribe(from: "chat:lobby")
publish()
Publish a message to a channel.
Parameters
to (String) - Channel name
event (String) - Event name
payload ([String: Any]) - Message payload as dictionary
Example
try await insforge.realtime.publish(
to: "chat:lobby",
event: "message.new",
payload: [
"text": "Hello everyone!",
"author": currentUser.name,
"timestamp": Date().timeIntervalSince1970
]
)
disconnect()
Disconnect from the realtime server.
Example
await insforge.realtime.disconnect()
Channel API
The high-level Channel API provides a more structured way to work with realtime features including broadcast messages and Postgres changes.
channel()
Get or create a channel instance.
let channel = await insforge.realtime.channel("chat:lobby")
removeChannel()
Remove a channel.
await insforge.realtime.removeChannel("chat:lobby")
RealtimeChannel
subscribe()
Subscribe to the channel. Must be called before receiving messages.
let channel = await insforge.realtime.channel("chat:lobby")
try await channel.subscribe()
unsubscribe()
Unsubscribe from the channel.
await channel.unsubscribe()
Broadcast Messages
Listening for Broadcasts
Use broadcast(event:) to receive broadcast messages as an AsyncStream.
let channel = await insforge.realtime.channel("chat:lobby")
try await channel.subscribe()
// Listen for specific event
for await message in await channel.broadcast(event: "new_message") {
print("Event: \(message.event)")
print("Payload: \(message.payload)")
// Decode to typed struct
if let chatMessage = try? message.decode(ChatMessage.self) {
print("Message: \(chatMessage.text)")
}
}
// Listen for all events (wildcard)
for await message in await channel.broadcast(event: "*") {
print("Received: \(message.event)")
}
Sending Broadcasts
Send broadcast messages with typed or dictionary payloads.
// With Encodable struct
struct ChatMessage: Codable {
let text: String
let author: String
}
try await channel.broadcast(
event: "new_message",
message: ChatMessage(text: "Hello!", author: "John")
)
// With dictionary
try await channel.broadcast(
event: "typing",
message: ["userId": userId, "isTyping": true]
)
Postgres Changes
Listen for real-time database changes using typed actions.
Action Types
InsertAction<T> - New record inserted
UpdateAction<T> - Record updated (includes record and oldRecord)
DeleteAction<T> - Record deleted (includes oldRecord)
SelectAction<T> - Record selected
AnyAction<T> - Any of the above
Listening for Changes
struct Todo: Codable, Sendable {
let id: String
let title: String
let completed: Bool
}
let channel = await insforge.realtime.channel("db-changes")
try await channel.subscribe()
// Listen for inserts only
for await action in await channel.postgresChange(
InsertAction<Todo>.self,
schema: "public",
table: "todos"
) {
print("New todo: \(action.record.title)")
}
// Listen for updates
for await action in await channel.postgresChange(
UpdateAction<Todo>.self,
schema: "public",
table: "todos"
) {
print("Updated: \(action.oldRecord.title) -> \(action.record.title)")
}
// Listen for deletes
for await action in await channel.postgresChange(
DeleteAction<Todo>.self,
schema: "public",
table: "todos"
) {
print("Deleted: \(action.oldRecord.title)")
}
// Listen for any change
for await action in await channel.postgresChange(
AnyAction<Todo>.self,
schema: "public",
table: "todos"
) {
switch action {
case .insert(let insert):
print("Inserted: \(insert.record.title)")
case .update(let update):
print("Updated: \(update.record.title)")
case .delete(let delete):
print("Deleted: \(delete.oldRecord.title)")
case .select(let select):
print("Selected: \(select.record.title)")
}
}
Models Reference
RealtimeMessage
public struct RealtimeMessage: Codable, Sendable {
let id: String?
let eventName: String?
let channelName: String?
let payload: [String: AnyCodable]?
let senderType: String?
let senderId: String?
let createdAt: Date?
}
BroadcastMessage
public struct BroadcastMessage: Sendable {
let event: String
let payload: [String: AnyCodable]
let senderId: String?
// Decode payload to typed struct
func decode<T: Decodable>(_ type: T.Type) throws -> T
}
InsertAction
public struct InsertAction<Record: Codable & Sendable>: Codable, Sendable {
let type: String // "INSERT"
let schema: String
let table: String
let record: Record
let commitTimestamp: String?
}
UpdateAction
public struct UpdateAction<Record: Codable & Sendable>: Codable, Sendable {
let type: String // "UPDATE"
let schema: String
let table: String
let record: Record // New values
let oldRecord: Record // Previous values
let commitTimestamp: String?
}
DeleteAction
public struct DeleteAction<Record: Codable & Sendable>: Codable, Sendable {
let type: String // "DELETE"
let schema: String
let table: String
let oldRecord: Record // Deleted record
let commitTimestamp: String?
}
Channel
public struct Channel: Codable, Sendable {
let id: String
let pattern: String
let description: String?
let webhookUrls: [String]?
let enabled: Bool
let createdAt: Date
let updatedAt: Date
}
SwiftUI Integration
Chat Room View
import SwiftUI
struct ChatRoomView: View {
@StateObject private var viewModel = ChatViewModel()
@State private var messageText = ""
var body: some View {
VStack {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(viewModel.messages, id: \.id) { message in
ChatMessageView(message: message)
}
}
.padding()
}
HStack {
TextField("Message", text: $messageText)
.textFieldStyle(.roundedBorder)
Button("Send") {
Task {
await viewModel.sendMessage(messageText)
messageText = ""
}
}
}
.padding()
}
.task {
await viewModel.connect()
}
.onDisappear {
Task {
await viewModel.disconnect()
}
}
}
}
@MainActor
class ChatViewModel: ObservableObject {
@Published var messages: [ChatMessage] = []
private let roomId = "room-1"
private var channel: RealtimeChannel?
private var messageTask: Task<Void, Never>?
struct ChatMessage: Codable, Identifiable {
let id: String
let sender: String
let text: String
let timestamp: Date
}
func connect() async {
do {
try await insforge.realtime.connect()
channel = await insforge.realtime.channel("chat:\(roomId)")
try await channel?.subscribe()
// Start listening for messages
messageTask = Task {
guard let channel = channel else { return }
for await message in await channel.broadcast(event: "new_message") {
if let chatMessage = try? message.decode(ChatMessage.self) {
messages.append(chatMessage)
}
}
}
} catch {
print("Connection error: \(error)")
}
}
func sendMessage(_ text: String) async {
guard let channel = channel else { return }
let message = ChatMessage(
id: UUID().uuidString,
sender: currentUser.name,
text: text,
timestamp: Date()
)
do {
try await channel.broadcast(event: "new_message", message: message)
} catch {
print("Send error: \(error)")
}
}
func disconnect() async {
messageTask?.cancel()
await channel?.unsubscribe()
await insforge.realtime.disconnect()
}
}
Order Tracking View
struct OrderTrackingView: View {
let orderId: String
@State private var status: String = "pending"
@State private var updates: [StatusUpdate] = []
struct StatusUpdate: Codable, Identifiable {
let id: String
let status: String
let timestamp: Date
}
var body: some View {
VStack {
Text("Order Status: \(status)")
.font(.headline)
List(updates) { update in
VStack(alignment: .leading) {
Text(update.status)
.font(.subheadline)
Text(update.timestamp, style: .time)
.font(.caption)
}
}
}
.task {
await subscribeToUpdates()
}
}
func subscribeToUpdates() async {
do {
try await insforge.realtime.connect()
await insforge.realtime.subscribe(to: "order:\(orderId)") { message in
if message.eventName == "status_changed",
let payload = message.payload,
let newStatus = payload["status"]?.value as? String {
Task { @MainActor in
status = newStatus
updates.append(StatusUpdate(
id: UUID().uuidString,
status: newStatus,
timestamp: Date()
))
}
}
}
} catch {
print("Error: \(error)")
}
}
}
Database Sync View
struct TodoListView: View {
@State private var todos: [Todo] = []
private var changeTask: Task<Void, Never>?
struct Todo: Codable, Sendable, Identifiable {
let id: String
let title: String
let completed: Bool
}
var body: some View {
List(todos) { todo in
HStack {
Text(todo.title)
Spacer()
if todo.completed {
Image(systemName: "checkmark.circle.fill")
}
}
}
.task {
await loadTodos()
await subscribeToChanges()
}
}
func loadTodos() async {
do {
todos = try await insforge.database
.from("todos")
.select()
.order("createdAt", ascending: false)
.execute()
} catch {
print("Load error: \(error)")
}
}
func subscribeToChanges() async {
do {
try await insforge.realtime.connect()
let channel = await insforge.realtime.channel("todos-sync")
try await channel.subscribe()
// Listen for any changes to todos table
for await action in await channel.postgresChange(
AnyAction<Todo>.self,
schema: "public",
table: "todos"
) {
await MainActor.run {
switch action {
case .insert(let insert):
todos.insert(insert.record, at: 0)
case .update(let update):
if let index = todos.firstIndex(where: { $0.id == update.record.id }) {
todos[index] = update.record
}
case .delete(let delete):
todos.removeAll { $0.id == delete.oldRecord.id }
case .select:
break
}
}
}
} catch {
print("Subscription error: \(error)")
}
}
}