iOS Client
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:
- Open your project in Xcode
- Go to File → Add Package Dependencies...
- Enter:
https://github.com/solidb/ios-sdk.git - 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 |
|---|---|---|---|
host | String | localhost | Server hostname or IP |
port | UInt16 | 6745 | Server port |
timeout | TimeInterval | 30.0 | Connection timeout (seconds) |
enableCompression | Bool | true | Enable 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:) | Void | Create collection (.document or .edge) |
deleteCollection(name:) | Void | Delete collection |
collectionStats(name:) | Document | Get 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:) | Document | Insert document, returns with key |
save(collection:key:document:) | Document | Insert with custom key |
get(collection:key:) | Document | Get document by key |
delete(collection:key:) | Void | Delete 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.
UniFFI-powered Rust bindings deliver native speed for all operations.
Encrypted SQLite storage for offline data persistence.
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 |
|---|---|
lastWriteWins | Document with most recent timestamp wins |
firstWriteWins | First write wins, subsequent rejected |
serverWins | Always prefer server version |
clientWins | Always prefer local version |
custom | Provide 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: