Installation
Swift Package Manager
Add TrailBase to yourPackage.swift:
dependencies: [
.package(url: "https://github.com/trailbaseio/trailbase.git", branch: "main")
]
- File → Add Package Dependencies
- Enter:
https://github.com/trailbaseio/trailbase.git - Select the
TrailBaseproduct
Platform Support
- iOS 13.0+
- macOS 10.15+
- watchOS 6.0+
- tvOS 13.0+
- Mac Catalyst 13.0+
Initialization
Basic Client
import TrailBase
let client = try Client(site: "https://your-server.trailbase.io")
Client with Tokens
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
do {
let tokens = try await client.login(
email: "[email protected]",
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
try await client.logout()
Current User
if let user = client.user() {
print("User ID: \(user.sub)")
print("Email: \(user.email)")
}
Access Tokens
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
try await client.refreshAuthToken()
Record API
Define Your Record Types
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
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
// 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
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
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
try await posts.delete(recordId: .string("post-id"))
Filtering
// 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
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
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
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
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
public struct User: Hashable, Equatable {
let sub: String
let email: String
}
Tokens
public struct Tokens: Codable, Hashable, Equatable, Sendable {
let auth_token: String
let refresh_token: String?
let csrf_token: String?
}
ListResponse
public struct ListResponse<T: Decodable>: Decodable {
public let cursor: String?
public let total_count: Int64?
public let records: [T]
}
Pagination
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
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
Use
async/await for all network operations. The SDK is built with Swift’s modern concurrency in mind.Store tokens securely using Keychain Services, not UserDefaults. Consider using a package like KeychainAccess.
The client automatically refreshes auth tokens before they expire.
Example Application
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")
}
}