> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/trailbaseio/trailbase/llms.txt
> Use this file to discover all available pages before exploring further.

# Swift SDK

> Official Swift client for TrailBase

The official Swift SDK for TrailBase provides a type-safe, async/await client for accessing your TrailBase backend from iOS, macOS, watchOS, and tvOS applications.

## Installation

### Swift Package Manager

Add TrailBase to your `Package.swift`:

```swift theme={null}
dependencies: [
    .package(url: "https://github.com/trailbaseio/trailbase.git", branch: "main")
]
```

Or add it via Xcode:

1. File → Add Package Dependencies
2. Enter: `https://github.com/trailbaseio/trailbase.git`
3. Select the `TrailBase` product

### Platform Support

* iOS 13.0+
* macOS 10.15+
* watchOS 6.0+
* tvOS 13.0+
* Mac Catalyst 13.0+

## Initialization

### Basic Client

```swift theme={null}
import TrailBase

let client = try Client(site: "https://your-server.trailbase.io")
```

### Client with Tokens

```swift theme={null}
import TrailBase

let tokens = Tokens(
    auth_token: "your-auth-token",
    refresh_token: "your-refresh-token",
    csrf_token: "your-csrf-token"
)

let client = try await Client.withTokens(
    site: "https://your-server.trailbase.io",
    tokens: tokens
)
```

## Authentication

### Login

```swift theme={null}
do {
    let tokens = try await client.login(
        email: "user@example.com",
        password: "password"
    )
    print("Auth token: \(tokens.auth_token)")
    
    if let user = client.user() {
        print("Logged in as: \(user.email)")
    }
} catch {
    print("Login failed: \(error)")
}
```

### Logout

```swift theme={null}
try await client.logout()
```

### Current User

```swift theme={null}
if let user = client.user() {
    print("User ID: \(user.sub)")
    print("Email: \(user.email)")
}
```

### Access Tokens

```swift theme={null}
if let tokens = client.tokens() {
    // Persist tokens for later use
    let encoder = JSONEncoder()
    if let data = try? encoder.encode(tokens) {
        UserDefaults.standard.set(data, forKey: "trailbase_tokens")
    }
}
```

### Refresh Token

```swift theme={null}
try await client.refreshAuthToken()
```

## Record API

### Define Your Record Types

```swift theme={null}
import Foundation

struct Post: Codable {
    let id: String
    let title: String
    let content: String
    let author_id: String
    let created_at: Int64
}

struct NewPost: Codable {
    let title: String
    let content: String
}
```

### List Records

```swift theme={null}
let posts = RecordApi(client: client, name: "posts")

let pagination = Pagination(
    cursor: nil,
    limit: 10,
    offset: 0
)

let response: ListResponse<Post> = try await posts.list(
    pagination: pagination,
    order: ["-created_at"],
    count: true
)

print("Records: \(response.records.count)")
print("Total count: \(response.total_count ?? 0)")
print("Next cursor: \(response.cursor ?? "none")")
```

### Read a Record

```swift theme={null}
// String ID
let post: Post = try await posts.read(recordId: .string("post-id"))

// Integer ID
let post: Post = try await posts.read(recordId: .int(123))

// With expanded relationships
let postWithAuthor: Post = try await posts.read(
    recordId: .string("post-id"),
    expand: ["author"]
)

print("Title: \(post.title)")
```

### Create a Record

```swift theme={null}
let newPost = NewPost(
    title: "Hello World",
    content: "My first post from Swift"
)

let postId = try await posts.create(record: newPost)
print("Created post with ID: \(postId)")
```

### Update a Record

```swift theme={null}
struct PostUpdate: Codable {
    let title: String
}

let update = PostUpdate(title: "Updated Title")
try await posts.update(recordId: .string("post-id"), record: update)
```

### Delete a Record

```swift theme={null}
try await posts.delete(recordId: .string("post-id"))
```

## Filtering

```swift theme={null}
// Simple equality filter
let response: ListResponse<Post> = try await posts.list(
    filters: [
        .Filter(column: "author_id", value: userId)
    ]
)

// With comparison operators
let weekAgo = Date().addingTimeInterval(-7 * 24 * 60 * 60)
let timestamp = String(Int64(weekAgo.timeIntervalSince1970))

let recentPosts: ListResponse<Post> = try await posts.list(
    filters: [
        .Filter(
            column: "created_at",
            op: .GreaterThan,
            value: timestamp
        )
    ]
)

// LIKE operator for text search
let searchResults: ListResponse<Post> = try await posts.list(
    filters: [
        .Filter(
            column: "title",
            op: .Like,
            value: "%search%"
        )
    ]
)

// AND composite filter
let filtered: ListResponse<Post> = try await posts.list(
    filters: [
        .And(filters: [
            .Filter(column: "status", value: "published"),
            .Filter(column: "author_id", value: userId)
        ])
    ]
)

// OR composite filter
let filtered: ListResponse<Post> = try await posts.list(
    filters: [
        .Or(filters: [
            .Filter(column: "category", value: "tech"),
            .Filter(column: "category", value: "science")
        ])
    ]
)
```

### Available Comparison Operators

```swift theme={null}
public enum CompareOp {
    case Equal
    case NotEqual
    case LessThan
    case LessThanEqual
    case GreaterThan
    case GreaterThanEqual
    case Like
    case Regexp
    case StWithin      // Geospatial
    case StIntersects  // Geospatial
    case StContains    // Geospatial
}
```

## RecordId

```swift theme={null}
public enum RecordId: CustomStringConvertible {
    case string(String)
    case int(Int64)
    
    public var description: String {
        switch self {
        case .string(let id): return id
        case .int(let id): return String(id)
        }
    }
}
```

## Error Handling

```swift theme={null}
do {
    let post: Post = try await posts.read(recordId: .string("post-id"))
} catch ClientError.invalidStatusCode(let code, let body) {
    print("HTTP \(code): \(body ?? "no body")")
} catch ClientError.invalidJwt {
    print("Invalid JWT token")
} catch ClientError.unauthenticated {
    print("Not authenticated")
} catch {
    print("Error: \(error)")
}
```

### ClientError Enum

```swift theme={null}
public enum ClientError: Error {
    case invalidUrl
    case invalidStatusCode(code: Int, body: String?)
    case invalidResponse(String?)
    case invalidJwt
    case unauthenticated
    case invalidFilter(String)
}
```

## Type Definitions

### User

```swift theme={null}
public struct User: Hashable, Equatable {
    let sub: String
    let email: String
}
```

### Tokens

```swift theme={null}
public struct Tokens: Codable, Hashable, Equatable, Sendable {
    let auth_token: String
    let refresh_token: String?
    let csrf_token: String?
}
```

### ListResponse

```swift theme={null}
public struct ListResponse<T: Decodable>: Decodable {
    public let cursor: String?
    public let total_count: Int64?
    public let records: [T]
}
```

### Pagination

```swift theme={null}
public struct Pagination {
    public var cursor: String? = nil
    public var limit: UInt? = nil
    public var offset: UInt? = nil
    
    public init(cursor: String? = nil, limit: UInt? = nil, offset: UInt? = nil)
}
```

## SwiftUI Integration

```swift theme={null}
import SwiftUI
import TrailBase

struct Post: Codable, Identifiable {
    let id: String
    let title: String
    let content: String
}

class PostsViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var loading = false
    @Published var error: String?
    
    private var client: Client
    
    init() {
        self.client = try! Client(site: "https://your-server.trailbase.io")
    }
    
    func loadPosts() async {
        loading = true
        error = nil
        
        do {
            let posts = RecordApi(client: client, name: "posts")
            let response: ListResponse<Post> = try await posts.list(
                pagination: Pagination(limit: 20),
                order: ["-created_at"]
            )
            
            await MainActor.run {
                self.posts = response.records
                self.loading = false
            }
        } catch {
            await MainActor.run {
                self.error = error.localizedDescription
                self.loading = false
            }
        }
    }
}

struct PostsView: View {
    @StateObject private var viewModel = PostsViewModel()
    
    var body: some View {
        NavigationView {
            Group {
                if viewModel.loading {
                    ProgressView()
                } else if let error = viewModel.error {
                    Text("Error: \(error)")
                } else {
                    List(viewModel.posts) { post in
                        VStack(alignment: .leading) {
                            Text(post.title)
                                .font(.headline)
                            Text(post.content)
                                .font(.subheadline)
                                .foregroundColor(.gray)
                        }
                    }
                }
            }
            .navigationTitle("Posts")
            .task {
                await viewModel.loadPosts()
            }
        }
    }
}
```

## Best Practices

<Note>
  Use `async/await` for all network operations. The SDK is built with Swift's modern concurrency in mind.
</Note>

<Warning>
  Store tokens securely using Keychain Services, not UserDefaults. Consider using a package like KeychainAccess.
</Warning>

<Info>
  The client automatically refreshes auth tokens before they expire.
</Info>

## Example Application

```swift theme={null}
import Foundation
import TrailBase

struct Post: Codable {
    let id: String
    let title: String
    let content: String
    let published: Bool
}

@main
struct TrailBaseExample {
    static func main() async throws {
        // Initialize client
        let url = ProcessInfo.processInfo.environment["TRAILBASE_URL"] 
            ?? "http://localhost:4000"
        let client = try Client(site: url)
        
        // Login
        do {
            _ = try await client.login(
                email: ProcessInfo.processInfo.environment["TRAILBASE_EMAIL"]!,
                password: ProcessInfo.processInfo.environment["TRAILBASE_PASSWORD"]!
            )
            
            if let user = client.user() {
                print("Logged in as: \(user.email)")
            }
        } catch {
            print("Login failed: \(error)")
            return
        }
        
        // List posts
        let posts = RecordApi(client: client, name: "posts")
        
        let response: ListResponse<Post> = try await posts.list(
            pagination: Pagination(limit: 10),
            order: ["-created_at"],
            filters: [
                .Filter(column: "published", value: "true")
            ]
        )
        
        print("\nFound \(response.records.count) posts:")
        for post in response.records {
            print("- \(post.title)")
        }
        
        // Create a new post
        struct NewPost: Codable {
            let title: String
            let content: String
            let published: Bool
        }
        
        let newPost = NewPost(
            title: "Hello from Swift",
            content: "This post was created using the TrailBase Swift SDK",
            published: true
        )
        
        let newPostId = try await posts.create(record: newPost)
        print("\nCreated new post with ID: \(newPostId)")
        
        // Read the post
        let post: Post = try await posts.read(recordId: newPostId)
        print("Post title: \(post.title)")
        
        // Logout
        try await client.logout()
        print("\nLogged out")
    }
}
```
