iOS Client

Official SDK v0.5.0 iOS 14+

Getting Started

Installation

Swift Package Manager (Recommended)

Add SoliDB to your `Package.swift`:

dependencies: [
    .package(url: "https://github.com/solidb/ios-sdk.git", from: "0.5.0")
]

Or add via Xcode:

  1. Open your project in Xcode
  2. Go to FileAdd Package Dependencies...
  3. Enter: https://github.com/solidb/ios-sdk.git
  4. Click Add Package

CocoaPods

pod 'SoliDB', '~> 0.5.0'

Requirements: iOS 14.0+, macOS 11.0+, tvOS 14.0+, watchOS 7.0+. The SDK uses Swift 5.7+ and UniFFI for native Rust bindings.

Quick Start

import SoliDB

class ViewController: UIViewController {
    var client: SoliDBClient?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Task {
            await setupDatabase()
        }
    }
    
    func setupDatabase() async {
        do {
            // Create client with configuration
            let config = SoliDBConfig(
                host: "localhost",
                port: 6745,
                database: "mydb",
                username: "admin",
                password: "password"
            )
            
            client = try await SoliDBClient(config: config)
            
            // Basic CRUD operations
            let doc = try await client!.save(
                collection: "users",
                document: [
                    "name": "Alice",
                    "age": 30,
                    "email": "[email protected]"
                ]
            )
            print("Created document with key: \(doc.key)")
            
            // Retrieve document
            let user = try await client!.get(
                collection: "users",
                key: doc.key
            )
            print("Retrieved: \(user["name"])")
            
            // Query with SDBQL
            let results = try await client!.query(
                "FOR u IN users FILTER u.age > @min RETURN u",
                bindVars: ["min": 25]
            )
            print("Found \(results.count) users")
            
        } catch {
            print("Error: \(error)")
        }
    }
}

Connection Management

// Configure connection
let config = SoliDBConfig(
    host: "localhost",
    port: 6745,
    database: "mydb",
    username: "admin",
    password: "password",
    timeout: 30.0,
    enableCompression: true
)

// Create client
let client = try await SoliDBClient(config: config)

// Check connection health
let isHealthy = try await client.ping()
print("Connection healthy: \(isHealthy)")

// Disconnect when done
try await client.disconnect()
Property Type Default Description
hostStringlocalhostServer hostname or IP
portUInt166745Server port
timeoutTimeInterval30.0Connection timeout (seconds)
enableCompressionBooltrueEnable MessagePack compression

Authentication

// Authentication is handled during client initialization
let config = SoliDBConfig(
    host: "localhost",
    port: 6745,
    database: "mydb",      // Target database
    username: "admin",     // Username
    password: "password"   // Password
)

let client = try await SoliDBClient(config: config)

// The session remains authenticated until disconnected
// Token is automatically refreshed when needed

Core Operations

Collection Operations

// List collections in a database
let collections = try await client.listCollections()
// => ["users", "orders", "products"]

// Create a document collection
try await client.createCollection(name: "products")

// Create an edge collection (for graphs)
try await client.createCollection(
    name: "relationships",
    type: .edge
)

// Delete a collection
try await client.deleteCollection(name: "old_collection")

// Get collection statistics
let stats = try await client.collectionStats(name: "users")
print("Document count: \(stats["document_count"])")
Method Returns Description
listCollections()[String]List collections in database
createCollection(name:type:)VoidCreate collection (.document or .edge)
deleteCollection(name:)VoidDelete collection
collectionStats(name:)DocumentGet collection statistics

Document Operations (CRUD)

// SAVE - Create or update a document
let doc = try await client.save(
    collection: "users",
    document: [
        "name": "Alice",
        "email": "[email protected]",
        "age": 30,
        "tags": ["developer", "ios"]
    ]
)
print("Key: \(doc.key)")  // Auto-generated key

// SAVE with custom key
let docWithKey = try await client.save(
    collection: "users",
    key: "user-123",
    document: [
        "name": "Bob",
        "email": "[email protected]"
    ]
)

// GET - Retrieve a document by key
let user = try await client.get(
    collection: "users",
    key: "user-123"
)
print("Name: \(user["name"])")

// DELETE - Remove a document
try await client.delete(
    collection: "users",
    key: "user-123"
)

// LIST - Paginated document listing
let (docs, total) = try await client.list(
    collection: "users",
    limit: 50,
    offset: 0
)
print("Showing \(docs.count) of \(total) documents")
Method Returns Description
save(collection:document:)DocumentInsert document, returns with key
save(collection:key:document:)DocumentInsert with custom key
get(collection:key:)DocumentGet document by key
delete(collection:key:)VoidDelete document
list(collection:limit:offset:)([Document], Int)List documents with pagination

SDBQL Queries

// Simple query
let users = try await client.query("FOR u IN users RETURN u")

// Query with bind variables (recommended for security)
let results = try await client.query(
    """
    FOR u IN users
    FILTER u.age >= @min_age AND u.status == @status
    SORT u.created_at DESC
    LIMIT @limit
    RETURN { name: u.name, email: u.email }
    """,
    bindVars: [
        "min_age": 18,
        "status": "active",
        "limit": 100
    ]
)

// Aggregation query
let stats = try await client.query(
    """
    FOR u IN users
    COLLECT status = u.status WITH COUNT INTO count
    RETURN { status, count }
    """
)

// Join query
let orders = try await client.query(
    """
    FOR o IN orders
    FOR u IN users FILTER u._key == o.user_id
    RETURN { order: o, user: u.name }
    """
)

Live Sync & Subscriptions

// Subscribe to live query updates
let subscription = try await client.sync.subscribe(
    query: "FOR u IN users FILTER u.status == 'online' RETURN u"
) { result in
    switch result {
    case .success(let docs):
        print("Received \(docs.count) updates")
        // Update your UI
    case .failure(let error):
        print("Sync error: \(error)")
    }
}

// Subscribe to collection changes
let collectionSub = try await client.sync.subscribe(
    collection: "users",
    filter: ["status": "active"]
) { change in
    switch change.type {
    case .insert:
        print("New document: \(change.document.key)")
    case .update:
        print("Updated: \(change.document.key)")
    case .delete:
        print("Deleted: \(change.key)")
    }
}

// Unsubscribe when done
subscription.cancel()
collectionSub.cancel()

// Manual sync (pull latest changes)
try await client.sync.pull()

// Push local changes immediately
try await client.sync.push()
Method Description
subscribe(query:handler:)Subscribe to query results
subscribe(collection:filter:handler:)Subscribe to collection changes
pull()Pull latest changes from server
push()Push local changes to server

Offline-First Sync

Offline-First Architecture

SoliDB iOS SDK provides automatic offline-first capabilities. All operations work seamlessly offline and sync when connectivity is restored.

// Enable offline-first mode in configuration
let config = SoliDBConfig(
    host: "localhost",
    port: 6745,
    database: "mydb",
    username: "admin",
    password: "password",
    offlineMode: .enabled,           // Enable offline support
    enableLocalCache: true,          // Cache documents locally
    syncInterval: 30.0               // Auto-sync every 30 seconds
)

// All operations work offline
// Changes are automatically queued and synced
let doc = try await client.save(
    collection: "notes",
    document: ["title": "Important Note", "content": "..."]
)
// Works even without network! Automatically syncs later.
Native Performance

UniFFI-powered Rust bindings deliver native speed for all operations.

Local SQLite

Encrypted SQLite storage for offline data persistence.

Auto-Sync

Automatic background sync with configurable intervals.

Conflict Resolution

When the same document is modified offline on multiple devices, SoliDB provides configurable conflict resolution strategies.

// Configure conflict resolution strategy
let config = SoliDBConfig(
    // ... other options
    conflictResolution: .lastWriteWins,     // Default: last write wins
    // Other options:
    // .firstWriteWins
    // .serverWins
    // .clientWins
    // .custom(callback: myResolver)
)

// Custom conflict resolver
let customConfig = SoliDBConfig(
    conflictResolution: .custom { serverDoc, localDoc in
        // Implement your own logic
        if let localModified = localDoc["modified_at"] as? Date,
           let serverModified = serverDoc["modified_at"] as? Date {
            return localModified > serverModified ? localDoc : serverDoc
        }
        return localDoc  // Default to local
    }
)
Strategy Description
lastWriteWinsDocument with most recent timestamp wins
firstWriteWinsFirst write wins, subsequent rejected
serverWinsAlways prefer server version
clientWinsAlways prefer local version
customProvide your own resolution logic

Delta Sync

Delta sync only transmits changed fields, dramatically reducing bandwidth and improving sync speed.

// Delta sync is enabled by default
let config = SoliDBConfig(
    // ... other options
    enableDeltaSync: true,              // Only sync changed fields
    deltaSyncThreshold: 1024            // Use delta for docs > 1KB
)

// Manual delta sync control
// Force full sync when needed
try await client.sync.pullFull()

// Sync specific collections with delta
try await client.sync.pull(
    collections: ["users", "settings"],
    mode: .delta
)

// Get sync statistics
let syncStats = client.sync.statistics
print("Bytes saved by delta sync: \(syncStats.bytesSaved)")
print("Documents synced: \(syncStats.documentsSynced)")

Sync Configuration

let config = SoliDBConfig(
    host: "localhost",
    port: 6745,
    database: "mydb",
    username: "admin",
    password: "password",
    
    // Offline settings
    offlineMode: .enabled,
    enableLocalCache: true,
    localCacheSize: 100 * 1024 * 1024,  // 100 MB cache limit
    
    // Sync settings
    syncInterval: 30.0,                  // Auto-sync every 30s
    syncOnConnectivityChange: true,      // Sync when going online
    retryAttempts: 5,                    // Retry failed syncs
    retryDelay: 2.0,                     // Initial retry delay (seconds)
    
    // Conflict & delta
    conflictResolution: .lastWriteWins,
    enableDeltaSync: true,
    
    // Network
    enableCompression: true,
    timeout: 30.0
)

Examples

Complete Todo App

A fully functional Todo app demonstrating offline-first capabilities, real-time sync, and CRUD operations.

import SwiftUI
import SoliDB

// MARK: - Models

struct Todo: Identifiable, Codable {
    let id: String
    var title: String
    var completed: Bool
    var createdAt: Date
    
    init(from document: Document) {
        self.id = document.key
        self.title = document["title"] as? String ?? ""
        self.completed = document["completed"] as? Bool ?? false
        self.createdAt = document["created_at"] as? Date ?? Date()
    }
    
    func toDocument() -> [String: Any] {
        [
            "title": title,
            "completed": completed,
            "created_at": createdAt
        ]
    }
}

// MARK: - ViewModel

@MainActor
class TodoViewModel: ObservableObject {
    @Published var todos: [Todo] = []
    @Published var isSyncing = false
    @Published var errorMessage: String?
    
    private var client: SoliDBClient?
    private var subscription: SyncSubscription?
    
    func initialize() async {
        let config = SoliDBConfig(
            host: "api.example.com",
            port: 6745,
            database: "todoapp",
            username: "user",
            password: "pass",
            offlineMode: .enabled,
            syncInterval: 10.0
        )
        
        do {
            client = try await SoliDBClient(config: config)
            await loadTodos()
            await subscribeToChanges()
        } catch {
            errorMessage = "Failed to connect: \(error)"
        }
    }
    
    func loadTodos() async {
        guard let client = client else { return }
        
        do {
            let results = try await client.query(
                "FOR t IN todos SORT t.created_at DESC RETURN t"
            )
            todos = results.map { Todo(from: $0) }
        } catch {
            errorMessage = "Failed to load: \(error)"
        }
    }
    
    func subscribeToChanges() async {
        guard let client = client else { return }
        
        do {
            subscription = try await client.sync.subscribe(
                collection: "todos"
            ) { [weak self] change in
                Task { @MainActor in
                    await self?.loadTodos()
                }
            }
        } catch {
            print("Subscription error: \(error)")
        }
    }
    
    func addTodo(title: String) async {
        guard let client = client else { return }
        
        let todo = [
            "title": title,
            "completed": false,
            "created_at": Date()
        ]
        
        do {
            _ = try await client.save(collection: "todos", document: todo)
            // Real-time update will come through subscription
        } catch {
            errorMessage = "Failed to add: \(error)"
        }
    }
    
    func toggleTodo(_ todo: Todo) async {
        guard let client = client else { return }
        
        var updated = todo
        updated.completed.toggle()
        
        do {
            _ = try await client.save(
                collection: "todos",
                key: todo.id,
                document: updated.toDocument()
            )
        } catch {
            errorMessage = "Failed to update: \(error)"
        }
    }
    
    func deleteTodo(_ todo: Todo) async {
        guard let client = client else { return }
        
        do {
            try await client.delete(collection: "todos", key: todo.id)
        } catch {
            errorMessage = "Failed to delete: \(error)"
        }
    }
    
    func syncNow() async {
        guard let client = client else { return }
        isSyncing = true
        
        do {
            try await client.sync.push()
            try await client.sync.pull()
            await loadTodos()
        } catch {
            errorMessage = "Sync failed: \(error)"
        }
        
        isSyncing = false
    }
}

// MARK: - Views

struct TodoListView: View {
    @StateObject private var viewModel = TodoViewModel()
    @State private var newTodoTitle = ""
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.todos) { todo in
                    TodoRow(todo: todo) {
                        await viewModel.toggleTodo(todo)
                    }
                }
                .onDelete { indexSet in
                    for index in indexSet {
                        Task {
                            await viewModel.deleteTodo(viewModel.todos[index])
                        }
                    }
                }
            }
            .navigationTitle("Todos")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: { Task { await viewModel.syncNow() } }) {
                        Image(systemName: "arrow.clockwise")
                            .rotationEffect(.degrees(viewModel.isSyncing ? 360 : 0))
                            .animation(.linear(duration: 1).repeatForever(autoreverses: false), value: viewModel.isSyncing)
                    }
                    .disabled(viewModel.isSyncing)
                }
            }
            .task {
                await viewModel.initialize()
            }
        }
    }
}

struct TodoRow: View {
    let todo: Todo
    let onToggle: () async -> Void
    
    var body: some View {
        HStack {
            Button(action: { Task { await onToggle() } }) {
                Image(systemName: todo.completed ? "checkmark.circle.fill" : "circle")
                    .foregroundColor(todo.completed ? .green : .gray)
            }
            
            Text(todo.title)
                .strikethrough(todo.completed)
                .foregroundColor(todo.completed ? .gray : .primary)
            
            Spacer()
        }
    }
}

SwiftUI Integration

// Property wrapper for reactive sync
@propertyWrapper
struct SoliDBQuery: DynamicProperty {
    @StateObject private var watcher: QueryWatcher
    
    var wrappedValue: [T] { watcher.results }
    var projectedValue: QueryWatcher { watcher }
    
    init(query: String, collection: String) {
        _watcher = StateObject(wrappedValue: QueryWatcher(query: query, collection: collection))
    }
}

class QueryWatcher: ObservableObject {
    @Published var results: [T] = []
    private var subscription: SyncSubscription?
    
    init(query: String, collection: String) {
        Task {
            await setupSubscription(query: query, collection: collection)
        }
    }
    
    private func setupSubscription(query: String, collection: String) async {
        // Setup live query subscription
    }
}

// Usage in SwiftUI
struct ContentView: View {
    @SoliDBQuery(
        query: "FOR t IN todos FILTER !t.completed RETURN t",
        collection: "todos"
    )
    var incompleteTodos
    
    var body: some View {
        List(incompleteTodos) { todo in
            Text(todo.title)
        }
    }
}

Error Handling

do {
    let client = try await SoliDBClient(config: config)
} catch SoliDBError.connectionFailed(let message) {
    print("Connection failed: \(message)")
} catch SoliDBError.authenticationFailed(let message) {
    print("Auth failed: \(message)")
} catch SoliDBError.documentNotFound(let key) {
    print("Document not found: \(key)")
} catch SoliDBError.networkError(let message) {
    print("Network error: \(message)")
    // Will auto-retry in offline mode
} catch SoliDBError.syncConflict(let message) {
    print("Sync conflict: \(message)")
    // Resolve manually if needed
} catch {
    print("Unknown error: \(error)")
}

Permissions & Setup

Info.plist Configuration

Add the following to your Info.plist for network permissions and background sync:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>your-server.com</key>
        <dict>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <false/>
        </dict>
    </dict>
</dict>

<!-- Background sync (optional) -->
<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
</array>

App Sandbox (macOS)

If using macOS, ensure your entitlements allow network connections:

<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<false/>

Next Steps

Explore more features of the SoliDB iOS SDK: