OddSockets Swift SDK

Official Swift SDK for OddSockets real-time messaging platform

Swift 5.7+ Swift Package Manager iOS 15+ High Performance Async/Await

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

swift
// 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

ruby
# Podfile
pod 'OddSockets', '~> 0.1.0'
text
# Cartfile
github "jyswee/oddsockets-swift-sdk" ~> 0.1.0

Quick Start

Basic Usage

swift
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

swift
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

swift
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

swift
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

swift
// 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, stopTypinguser_typing, user_stopped_typing
  • Reactions: addReaction, removeReaction, getReactionsreaction_added, reaction_removed
  • Threads: threadReply, getThread, subscribeThread, followThread, unfollowThread, markThreadReadthread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receipts: markRead, markAllRead, getUnreadCountsuser_read, unread_count_updated, all_marked_read
  • Messages: editMessage, deleteMessage, pinMessage, unpinMessage, getPinnedMessagesmessage_edited, message_deleted, message_pinned, message_unpinned
  • Presence & status: setStatus, setCustomStatus, clearCustomStatus, setDND, clearDND, getUserPresenceuser_status_changed, custom_status_updated, dnd_status_changed
  • Channels: createChannel, updateChannel, archiveChannel, inviteToChannel, joinChannel, leaveChannel, getChannelMemberschannel_created, channel_updated, user_invited, user_joined_channel, user_left_channel
  • DMs: createDM, sendDM, getDMConversationsdm_created, dm_received
  • Notifications: subscribeNotifications, getNotifications, markNotificationRead, clearNotificationsnotification, 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 → ack challenge_create_success
  • reportProgress: fire-and-forget metric progress (no ack)
  • completeChallenge: finalize with an outcome → ack challenge_complete_success
  • unlockAchievement: fire-and-forget; pass percentComplete (0–100) (no ack)
  • getStandings: request top-N + caller rank → ack challenge_standings_success
  • getAchievements: query achievement state → ack achievement_state
  • sendChallengeInvite: directed invite to another user → ack challenge_invite_success
  • replyChallengeInvite: accept/decline an invite → ack challenge_reply_success
  • cancelChallengeInvite: cancel a sent invite → ack challenge_invite_cancel_success
  • getChallengeInvites: list pending invites → ack challenge_invites

Outcome vocabulary

The outcome passed to completeChallenge is one of:

  • completed — win (rank 1)
  • failed — loss
  • tied — draw
  • conceded — resign / concede
  • expired — 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:

<50ms
Latency
99.9%
Uptime
32KB
Max Message
1M+
Messages/sec

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

swift
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

swift
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

swift
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

swift
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)")
        }
    }
}