OddSockets Swift SDK
Official Swift SDK for OddSockets real-time messaging platform
Overview & Features
The OddSockets Swift SDK provides a modern, native interface for real-time messaging in iOS, macOS, watchOS, and tvOS applications, built with Swift's latest async/await patterns and Combine framework.
Modern Swift
Built with Swift 5.7+ featuring async/await, actors, and modern concurrency patterns.
Multi-Platform
Supports iOS 15+, macOS 12+, watchOS 8+, and tvOS 15+ with unified API.
SwiftUI Ready
ObservableObject conformance and @Published properties for seamless SwiftUI integration.
High Performance
Optimized for low latency with efficient WebSocket connections and smart routing.
Cost Effective
No per-message pricing, industry-standard 32KB message limits, transparent pricing.
Automatic Failover
Built-in redundancy and intelligent error handling for 99.9% uptime.
Installation
// Package.swift
dependencies: [
.package(url: "https://github.com/jyswee/oddsockets-swift-sdk.git", from: "0.1.0")
]
Or add via Xcode: File → Add Package Dependencies → Enter URL
# Podfile
pod 'OddSockets', '~> 0.1.0'
# Cartfile
github "jyswee/oddsockets-swift-sdk" ~> 0.1.0
Quick Start
Basic Usage
import OddSockets
// Create configuration
let config = OddSocketsConfig.Builder()
.apiKey("ak_live_1234567890abcdef")
.build()
// Initialize client
let client = OddSockets(config: config)
// Get channel
let channel = client.channel("my-channel")
// Subscribe to messages
try await channel.subscribe { message in
print("Received: \(message.data)")
}
// Publish a message
try await channel.publish("Hello, World!")
SwiftUI Integration
import SwiftUI
import OddSockets
struct ChatView: View {
@StateObject private var channel: OddSocketsChannel
@State private var messageText = ""
init() {
let config = OddSocketsConfig.Builder()
.apiKey("ak_live_1234567890abcdef")
.build()
let client = OddSockets(config: config)
_channel = StateObject(wrappedValue: client.channel("chat"))
}
var body: some View {
VStack {
List(channel.messageHistory, id: \.id) { message in
Text(message.data?.description ?? "")
}
HStack {
TextField("Message", text: $messageText)
Button("Send") {
Task {
try await channel.publish(messageText)
messageText = ""
}
}
}
}
.task {
try? await channel.subscribe { message in
// Messages automatically update via @Published
}
}
}
}
Combine Integration
import Combine
import OddSockets
class ChatViewModel: ObservableObject {
@Published var messages: [Message] = []
private var cancellables = Set()
private let channel: OddSocketsChannel
init() {
let config = OddSocketsConfig.Builder()
.apiKey("ak_live_1234567890abcdef")
.build()
let client = OddSockets(config: config)
channel = client.channel("chat")
// Subscribe to message publisher
channel.messagePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] message in
self?.messages.append(message)
}
.store(in: &cancellables)
}
func sendMessage(_ text: String) async {
try? await channel.publish(text)
}
}
Configuration
Client Configuration
let config = OddSocketsConfig.Builder()
.apiKey("ak_live_1234567890abcdef") // Required: Your OddSockets API key
.userId("user-123") // Optional: User identifier
.autoConnect(true) // Optional: Auto-connect on creation
.reconnectAttempts(5) // Optional: Max reconnection attempts
.heartbeatInterval(30) // Optional: Heartbeat interval (seconds)
.timeout(10) // Optional: Connection timeout (seconds)
.build()
Channel Options
// Subscribe with options
let subscribeOptions = SubscribeOptions.Builder()
.enablePresence(true) // Enable presence tracking
.retainHistory(true) // Retain message history
.filterExpression("user.premium == true") // Message filter expression
.build()
try await channel.subscribe(handler: { message in
print("Received: \(message)")
}, options: subscribeOptions)
// Publish with options
let publishOptions = PublishOptions.Builder()
.ttl(3600) // Time to live (seconds)
.metadata(["priority": "high"]) // Additional metadata
.storeInHistory(true) // Store in message history
.build()
try await channel.publish(message, options: publishOptions)
Examples
Explore comprehensive examples demonstrating the OddSockets Swift SDK in production applications:
Enhanced Features
Beyond core pub/sub, OddSockets ships a Slack-like enhanced surface — reactions, typing indicators, threads, read receipts, presence/status, notifications, DMs, channel management, message editing and search. It lives on client.enhanced, all travelling over the same live socket as core messaging. You send an action with a client.enhanced.* method (camelCase, labelled arguments) and receive the paired broadcast with client.on("<event>") { data in ... } — the worker fans every enhanced broadcast onto the client's raw event surface as Any (typically a [String: Any]).
Typing & Reactions
import OddSockets
let config = OddSocketsConfig(apiKey: "ak_your_api_key_here", userId: "alice", autoConnect: false)
let client = try OddSocketsClient(config: config)
try await client.connect()
let channel = try client.channel("room-42")
try await channel.subscribe { message in
print("Received: \(message.data)")
}
// Receive-path: enhanced broadcasts arrive on the client's raw event surface
client.on("user_typing") { data in print("someone is typing: \(data)") }
client.on("reaction_added") { data in print("reaction added: \(data)") }
// Send-path: fire-and-forget enhanced actions over the live socket
client.enhanced.startTyping(userId: "alice", channel: "room-42")
client.enhanced.addReaction(messageId: "msg-1", channel: "room-42", emoji: ":thumbsup:", userId: "alice", userName: "Alice")
Threads
client.on("thread_reply") { data in print("new thread reply: \(data)") }
// async request methods return the worker ack as [String: Any]
let reply = try await client.enhanced.threadReply(
channel: "room-42",
parentMessageId: "msg-1",
message: "Replying in the thread",
userId: "alice",
userName: "Alice"
)
print("thread reply ack: \(reply)")
Fire-and-forget actions return Void; query and request-style methods (get*, search*, threadReply, createChannel, …) are async throws and return [String: Any] with the worker response.
- Typing:
startTyping,stopTyping→user_typing,user_stopped_typing - Reactions:
addReaction,removeReaction,getReactions→reaction_added,reaction_removed - Threads:
threadReply,getThread,subscribeThread,followThread,unfollowThread,markThreadRead→thread_reply,thread_subscribed,thread_followed,thread_read_updated - Read receipts:
markRead,markAllRead,getUnreadCounts→user_read,unread_count_updated,all_marked_read - Messages:
editMessage,deleteMessage,pinMessage,unpinMessage,getPinnedMessages→message_edited,message_deleted,message_pinned,message_unpinned - Presence & status:
setStatus,setCustomStatus,clearCustomStatus,setDND,clearDND,getUserPresence→user_status_changed,custom_status_updated,dnd_status_changed - Channels:
createChannel,updateChannel,archiveChannel,inviteToChannel,joinChannel,leaveChannel,getChannelMembers→channel_created,channel_updated,user_invited,user_joined_channel,user_left_channel - DMs:
createDM,sendDM,getDMConversations→dm_created,dm_received - Notifications:
subscribeNotifications,getNotifications,markNotificationRead,clearNotifications→notification,notification_read,notifications_cleared - Search:
searchMessages,searchInChannel,searchByUser,filterMessages(query results returned as[String: Any])
For any worker event not wrapped above, subscribe with the raw client.on("<event>") { data in ... } API — all enhanced broadcasts are forwarded onto the client surface.
Challenges & Leaderboards
Challenges, leaderboards and achievements build on the same live socket. The send side lives on the enhanced surface (client.enhanced.*); request/query methods are async throws and resolve with the worker's reply as [String: Any], while progress and achievement calls are fire-and-forget (Void). Inbound broadcasts arrive on the client event surface — subscribe with the client's normal client.on("<event>") { data in ... }.
// Receive-path: standings and lifecycle broadcasts land on the client surface
client.on("challenge_progress") { data in print("progress: \(data)") }
client.on("leaderboard_rank_change") { data in print("rank change: \(data)") }
// Create a challenge, report progress, read standings, then finalize
let created = try await client.enhanced.createChallenge(challengeId: "daily-sprint", metric: "score", ranked: true)
client.enhanced.reportProgress(challengeId: "daily-sprint", value: 4200)
let standings = try await client.enhanced.getStandings(challengeId: "daily-sprint", limit: 10)
let result = try await client.enhanced.completeChallenge(challengeId: "daily-sprint", outcome: "completed")
Request-style methods (createChallenge, completeChallenge, getStandings, getAchievements, sendChallengeInvite, replyChallengeInvite, cancelChallengeInvite, getChallengeInvites) are async throws and return the worker ack; reportProgress and unlockAchievement are fire-and-forget over the live socket.
Methods
createChallenge: create a challenge/leaderboard → ackchallenge_create_successreportProgress: fire-and-forget metric progress (no ack)completeChallenge: finalize with anoutcome→ ackchallenge_complete_successunlockAchievement: fire-and-forget; passpercentComplete(0–100) (no ack)getStandings: request top-N + caller rank → ackchallenge_standings_successgetAchievements: query achievement state → ackachievement_statesendChallengeInvite: directed invite to another user → ackchallenge_invite_successreplyChallengeInvite: accept/decline an invite → ackchallenge_reply_successcancelChallengeInvite: cancel a sent invite → ackchallenge_invite_cancel_successgetChallengeInvites: list pending invites → ackchallenge_invites
Outcome vocabulary
The outcome passed to completeChallenge is one of:
completed— win (rank 1)failed— losstied— drawconceded— resign / concedeexpired— timed out
Progressive achievements
unlockAchievement always emits the wire event achievement_unlock; the worker is authoritative and derives the outbound broadcast from percentComplete: below 100 it broadcasts achievement_progress (status in_progress); at 100 or when omitted it broadcasts achievement_unlock (status unlocked). You never emit achievement_progress yourself.
Inbound events
Subscribe to these with the client's normal client.on("<event>") { data in ... } API:
- Room broadcasts:
challenge_progress,leaderboard_rank_change,challenge_complete,achievement_unlock,achievement_progress - Directed (per-user):
challenge_invited,challenge_reply_received,challenge_invite_cancelled
Performance & Compatibility
OddSockets Swift SDK delivers superior performance with broad Apple platform compatibility:
Platform Support
- iOS 15.0+
- macOS 12.0+
- watchOS 8.0+
- tvOS 15.0+
Swift Features
- Swift 5.7+
- Async/Await
- Combine Framework
- SwiftUI Ready
Framework Integrations
The OddSockets Swift SDK works seamlessly with all Apple frameworks. Here are examples showing how to integrate with popular patterns:
SwiftUI
import SwiftUI
import OddSockets
struct ContentView: View {
@StateObject private var viewModel = ChatViewModel()
@State private var messageText = ""
var body: some View {
NavigationView {
VStack {
List(viewModel.messages, id: \.id) { message in
MessageRow(message: message)
}
HStack {
TextField("Type a message...", text: $messageText)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Send") {
Task {
await viewModel.sendMessage(messageText)
messageText = ""
}
}
.disabled(messageText.isEmpty)
}
.padding()
}
.navigationTitle("Chat")
}
.task {
await viewModel.connect()
}
}
}
@MainActor
class ChatViewModel: ObservableObject {
@Published var messages: [Message] = []
@Published var isConnected = false
private let client: OddSockets
private let channel: OddSocketsChannel
init() {
let config = OddSocketsConfig.Builder()
.apiKey("ak_live_1234567890abcdef")
.build()
client = OddSockets(config: config)
channel = client.channel("swiftui-chat")
}
func connect() async {
do {
try await channel.subscribe { [weak self] message in
await MainActor.run {
self?.messages.append(message)
}
}
isConnected = true
} catch {
print("Connection failed: \(error)")
}
}
func sendMessage(_ text: String) async {
do {
try await channel.publish(text)
} catch {
print("Send failed: \(error)")
}
}
}
UIKit
import UIKit
import OddSockets
import Combine
class ChatViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var messageTextField: UITextField!
@IBOutlet weak var sendButton: UIButton!
private var messages: [Message] = []
private var cancellables = Set()
private let client: OddSockets
private let channel: OddSocketsChannel
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
let config = OddSocketsConfig.Builder()
.apiKey("ak_live_1234567890abcdef")
.build()
client = OddSockets(config: config)
channel = client.channel("uikit-chat")
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
connectToChannel()
}
private func setupUI() {
tableView.dataSource = self
tableView.delegate = self
sendButton.addTarget(self, action: #selector(sendButtonTapped), for: .touchUpInside)
}
private func connectToChannel() {
// Subscribe to messages using Combine
channel.messagePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] message in
self?.messages.append(message)
self?.tableView.reloadData()
self?.scrollToBottom()
}
.store(in: &cancellables)
// Connect to channel
Task {
do {
try await channel.subscribe { _ in
// Messages handled by Combine publisher
}
} catch {
await MainActor.run {
self.showError("Connection failed: \(error)")
}
}
}
}
@objc private func sendButtonTapped() {
guard let text = messageTextField.text, !text.isEmpty else { return }
Task {
do {
try await channel.publish(text)
await MainActor.run {
self.messageTextField.text = ""
}
} catch {
await MainActor.run {
self.showError("Send failed: \(error)")
}
}
}
}
private func scrollToBottom() {
guard !messages.isEmpty else { return }
let indexPath = IndexPath(row: messages.count - 1, section: 0)
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
private func showError(_ message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
Combine
import Combine
import OddSockets
class ChatService: ObservableObject {
@Published var messages: [Message] = []
@Published var connectionStatus: ConnectionStatus = .disconnected
@Published var presenceInfo: PresenceInfo?
private let client: OddSockets
private let channel: OddSocketsChannel
private var cancellables = Set()
enum ConnectionStatus {
case disconnected, connecting, connected, error(String)
}
init(apiKey: String, channelName: String) {
let config = OddSocketsConfig.Builder()
.apiKey(apiKey)
.build()
client = OddSockets(config: config)
channel = client.channel(channelName)
setupPublishers()
}
private func setupPublishers() {
// Message publisher
channel.messagePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] message in
self?.messages.append(message)
}
.store(in: &cancellables)
// Presence publisher
channel.presencePublisher
.receive(on: DispatchQueue.main)
.assign(to: \.presenceInfo, on: self)
.store(in: &cancellables)
// Event publisher for connection status
channel.eventPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] (eventType, data) in
switch eventType {
case .connected:
self?.connectionStatus = .connected
case .disconnected:
self?.connectionStatus = .disconnected
default:
break
}
}
.store(in: &cancellables)
}
func connect() -> AnyPublisher {
connectionStatus = .connecting
return Future { [weak self] promise in
guard let self = self else {
promise(.failure(OddSocketsError.clientError("Service deallocated")))
return
}
Task {
do {
try await self.channel.subscribe { _ in
// Messages handled by publisher
}
promise(.success(()))
} catch {
await MainActor.run {
self.connectionStatus = .error(error.localizedDescription)
}
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}
func sendMessage(_ text: String) -> AnyPublisher {
return Future { [weak self] promise in
guard let self = self else {
promise(.failure(OddSocketsError.clientError("Service deallocated")))
return
}
Task {
do {
try await self.channel.publish(text)
promise(.success(()))
} catch {
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}
func disconnect() {
Task {
await channel.unsubscribe()
await MainActor.run {
self.connectionStatus = .disconnected
}
}
}
}
watchOS
import SwiftUI
import WatchKit
import OddSockets
struct WatchChatView: View {
@StateObject private var viewModel = WatchChatViewModel()
@State private var isShowingMessageInput = false
var body: some View {
NavigationView {
VStack {
if viewModel.messages.isEmpty {
Text("No messages yet")
.foregroundColor(.secondary)
} else {
List(viewModel.messages.suffix(10), id: \.id) { message in
VStack(alignment: .leading, spacing: 2) {
Text(message.data?.description ?? "")
.font(.caption)
Text(message.timestamp, style: .time)
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
Button("Send Message") {
isShowingMessageInput = true
}
.buttonStyle(.borderedProminent)
}
.navigationTitle("Chat")
.navigationBarTitleDisplayMode(.inline)
}
.sheet(isPresented: $isShowingMessageInput) {
MessageInputView { message in
Task {
await viewModel.sendMessage(message)
}
}
}
.task {
await viewModel.connect()
}
}
}
@MainActor
class WatchChatViewModel: ObservableObject {
@Published var messages: [Message] = []
@Published var isConnected = false
private let client: OddSockets
private let channel: OddSocketsChannel
init() {
let config = OddSocketsConfig.Builder()
.apiKey("ak_live_1234567890abcdef")
.build()
client = OddSockets(config: config)
channel = client.channel("watch-chat")
}
func connect() async {
do {
try await channel.subscribe { [weak self] message in
await MainActor.run {
self?.messages.append(message)
// Keep only last 20 messages for memory efficiency
if let messages = self?.messages, messages.count > 20 {
self?.messages = Array(messages.suffix(20))
}
}
}
isConnected = true
} catch {
print("Watch connection failed: \(error)")
}
}
func sendMessage(_ text: String) async {
do {
try await channel.publish(text)
} catch {
print("Watch send failed: \(error)")
}
}
}