关于 appwrite-swift
appwrite-swift 是面向 Apple 平台与 Swift 服务端应用的 Appwrite SDK,适用于开发客户端和服务端集成。它提供认证、TablesDB、文件存储、实时订阅与云函数接口,并包含 SSR 认证示例。
主要功能
- 01用户认证与管理
- 02TablesDB 数据操作与查询
- 03文件存储与权限设置
- 04团队管理与实时订阅
- 05调用 Appwrite 云函数
适用场景
- 01为 Apple 平台应用添加 Appwrite 认证
- 02在 Swift 服务端管理用户与数据
- 03订阅表行或文件的实时变化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ appwrite/ skills --skill appwrite-swift安装后在 Claude Code 中输入/appwrite-swift确认已加载。
详细介绍
Appwrite Swift SDK
安装
// Swift Package Manager — Package.swift
.package(url: "https://github.com/appwrite/sdk-for-swift", branch: "main")
设置客户端
客户端(Apple 平台)
import Appwrite
let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
服务端(Swift)
import Appwrite
let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject(ProcessInfo.processInfo.environment["APPWRITE_PROJECT_ID"]!)
.setKey(ProcessInfo.processInfo.environment["APPWRITE_API_KEY"]!)
代码示例
身份验证(客户端)
let account = Account(client)
// Signup
let user = try await account.create(userId: ID.unique(), email: "[email protected]", password: "password123", name: "User Name")
// Login
let session = try await account.createEmailPasswordSession(email: "[email protected]", password: "password123")
// OAuth
try await account.createOAuth2Session(provider: .google)
// Get current user
let me = try await account.get()
// Logout
try await account.deleteSession(sessionId: "current")
用户管理(服务端)
let users = Users(client)
// Create user
let user = try await users.create(userId: ID.unique(), email: "[email protected]", password: "password123", name: "User Name")
// List users
let list = try await users.list(queries: [Query.limit(25)])
// Get user
let fetched = try await users.get(userId: "[USER_ID]")
// Delete user
try await users.delete(userId: "[USER_ID]")
数据库操作
注意: 所有新代码都应使用
TablesDB(而非已弃用的Databases类)。仅当现有代码库已依赖Databases,或用户明确要求时,才使用Databases。提示: SDK 方法调用应优先使用命名参数(例如
databaseId: "...")。仅当现有代码库已使用位置参数,或用户明确要求时,才使用位置参数。
let tablesDB = TablesDB(client)
// Create database (server-side only)
let db = try await tablesDB.create(databaseId: ID.unique(), name: "My Database")
// Create row
let doc = try await tablesDB.createRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: ID.unique(), data: [
"title": "Hello",
"done": false
])
// Query rows
let results = try await tablesDB.listRows(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: [
Query.equal("done", value: false),
Query.limit(10)
])
// Get row
let row = try await tablesDB.getRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]")
// Update row
try await tablesDB.updateRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]", data: ["done": true])
// Delete row
try await tablesDB.deleteRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]")
字符串列类型
注意: 旧版
string类型已弃用。所有新列都应使用明确的列类型。
| 类型 | 最大字符数 | 索引 | 存储 |
|---|---|---|---|
varchar | 16,383 | 完整索引(如果大小 ≤ 768) | 行内 |
text | 16,383 | 仅前缀 | 页外 |
mediumtext | 4,194,303 | 仅前缀 | 页外 |
longtext | 1,073,741,823 | 仅前缀 | 页外 |
varchar存储在行内,并计入 64 KB 的行大小限制。适用于名称、slug 或标识符等较短且需要索引的字段。text、mediumtext和longtext存储在页外(行中仅存放一个 20 字节的指针),因此不会占用行大小限额。这些类型不需要指定size。
// Create table with explicit string column types
try await tablesDB.createTable(
databaseId: "[DATABASE_ID]",
tableId: ID.unique(),
name: "articles",
columns: [
["key": "title", "type": "varchar", "size": 255, "required": true],
["key": "summary", "type": "text", "required": false],
["key": "body", "type": "mediumtext", "required": false],
["key": "raw_data", "type": "longtext", "required": false],
]
)
查询方法
// Filtering
Query.equal("field", value: "value") // == (or pass array for IN)
Query.notEqual("field", value: "value") // !=
Query.lessThan("field", value: 100) // <
Query.lessThanEqual("field", value: 100) // <=
Query.greaterThan("field", value: 100) // >
Query.greaterThanEqual("field", value: 100) // >=
Query.between("field", start: 1, end: 100) // 1 <= field <= 100
Query.isNull("field") // is null
Query.isNotNull("field") // is not null
Query.startsWith("field", value: "prefix") // starts with
Query.endsWith("field", value: "suffix") // ends with
Query.contains("field", value: "sub") // contains
Query.search("field", value: "keywords") // full-text search (requires index)
// Sorting
Query.orderAsc("field")
Query.orderDesc("field")
// Pagination
Query.limit(25) // max rows (default 25, max 100)
Query.offset(0) // skip N rows
Query.cursorAfter("[ROW_ID]") // cursor pagination (preferred)
Query.cursorBefore("[ROW_ID]")
// Selection & Logic
Query.select(["field1", "field2"])
Query.or([Query.equal("a", value: 1), Query.equal("b", value: 2)]) // OR
Query.and([Query.greaterThan("age", value: 18), Query.lessThan("age", value: 65)]) // AND (default)
Query.relationship("comments", queries: [Query.equal("approved", value: true), Query.limit(25)]) // related documents
// Aggregations (optional alias)
Query.count("*", alias: "total")
Query.sum("amount", alias: "revenue")
Query.avg("score", alias: "avgScore")
Query.min("price", alias: "minPrice")
Query.max("price", alias: "maxPrice")
// Grouping
Query.groupBy(["status", "year"])
Query.having([Query.greaterThan("total", value: 1)])
Query.distinct()
// Joins (table id, left key, right key, operator, alias)
Query.join("orders", left: "$id", right: "customerId")
Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")
Query.leftJoin("orders", alias: "ord", on: [
Query.on("$id", "customerId"),
Query.equal("ord.status", value: "paid"),
])
// Fluent builder. Pass build() into list methods.
let queries = Query.builder()
.equal("status", value: "published")
.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")
.count("*", alias: "total")
.groupBy(["status"])
.limit(25)
.build()
_ = try await tablesDB.listRows(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: queries)
文件存储
let storage = Storage(client)
// Upload file
let file = try await storage.createFile(bucketId: "[BUCKET_ID]", fileId: ID.unique(), file: InputFile.fromPath("/path/to/file.png"))
// List files
let files = try await storage.listFiles(bucketId: "[BUCKET_ID]")
// Delete file
try await storage.deleteFile(bucketId: "[BUCKET_ID]", fileId: "[FILE_ID]")
InputFile 工厂方法
InputFile.fromPath("/path/to/file.png") // from filesystem path
InputFile.fromData(data, filename: "file.png", mimeType: "image/png") // from Data
团队
let teams = Teams(client)
// Create team
let team = try await teams.create(teamId: ID.unique(), name: "Engineering")
// List teams
let list = try await teams.list()
// Create membership (invite user by email)
let membership = try await teams.createMembership(
teamId: "[TEAM_ID]",
roles: ["editor"],
email: "[email protected]"
)
// List memberships
let members = try await teams.listMemberships(teamId: "[TEAM_ID]")
// Update membership roles
try await teams.updateMembership(teamId: "[TEAM_ID]", membershipId: "[MEMBERSHIP_ID]", roles: ["admin"])
// Delete team
try await teams.delete(teamId: "[TEAM_ID]")
基于角色的访问控制: 设置权限时,使用
Role.team("[TEAM_ID]")指定所有团队成员,或使用Role.team("[TEAM_ID]", "editor")指定特定团队角色。
实时订阅(客户端)
let realtime = Realtime(client)
// Subscribe to row changes
let subscription = try await realtime.subscribe(channels: [
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row()
]) { response in
print(response.events) // e.g. ["tablesdb.*.tables.*.rows.*.create"]
print(response.payload) // the affected resource
}
// Subscribe to multiple channels
let multi = try await realtime.subscribe(channels: [
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row(),
Channel.files(),
]) { response in /* ... */ }
// Cleanup
try await subscription.close()
可用频道:
| 频道 | 说明 |
|---|---|
account | 已通过身份验证的用户账户变更 |
tablesdb.[DB_ID].tables.[TABLE_ID].rows | 表中的所有行 |
tablesdb.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID] | 特定行 |
buckets.[BUCKET_ID].files | 存储桶中的所有文件 |
buckets.[BUCKET_ID].files.[FILE_ID] | 特定文件 |
teams | 用户所属团队的变更 |
teams.[TEAM_ID] | 特定团队 |
memberships | 用户的团队成员关系 |
functions.[FUNCTION_ID].executions | 函数执行更新 |
响应字段:events(数组)、payload(资源)、channels(匹配的频道)、timestamp(ISO 8601)。
无服务器函数(服务端)
let functions = Functions(client)
// Execute function
let execution = try await functions.createExecution(functionId: "[FUNCTION_ID]", body: "{\"key\": \"value\"}")
// List executions
let executions = try await functions.listExecutions(functionId: "[FUNCTION_ID]")
编写函数处理程序(Swift 运行时)
// Sources/main.swift — Appwrite Function entry point
func main(context: RuntimeContext) async throws -> RuntimeOutput {
// context.req.body — raw body (String)
// context.req.bodyJson — parsed JSON ([String: Any]?)
// context.req.headers — headers ([String: String])
// context.req.method — HTTP method
// context.req.path — URL path
// context.req.query — query params ([String: String])
context.log("Processing: \(context.req.method) \(context.req.path)")
if context.req.method == "GET" {
return context.res.json(["message": "Hello from Appwrite Function!"])
}
return context.res.json(["success": true]) // JSON
// context.res.text("Hello") // plain text
// context.res.empty() // 204
// context.res.redirect("https://...") // 302
}
服务端渲染(SSR)身份验证
使用服务端 Swift(Vapor、Hummingbird 等)的 SSR 应用使用服务端 SDK处理身份验证。你需要两个客户端:
- 管理客户端——使用 API 密钥创建会话,并绕过速率限制(可复用的单例)
- 会话客户端——使用会话 Cookie 代表用户执行操作(每个请求创建一个,绝不可共享)
import Appwrite
// Admin client (reusable)
let adminClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
.setKey(Environment.get("APPWRITE_API_KEY")!)
// Session client (create per-request)
let sessionClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
if let session = req.cookies["a_session_[PROJECT_ID]"]?.string {
sessionClient.setSession(session)
}
电子邮件/密码登录(Vapor)
app.post("login") { req async throws -> Response in
let body = try req.content.decode(LoginRequest.self)
let account = Account(adminClient)
let session = try await account.createEmailPasswordSession(
email: body.email,
password: body.password
)
// Cookie name must be a_session_<PROJECT_ID>
let response = Response(status: .ok, body: .init(string: "{\"success\": true}"))
response.cookies["a_session_[PROJECT_ID]"] = HTTPCookies.Value(
string: session.secret,
isHTTPOnly: true,
isSecure: true,
sameSite: .strict,
path: "/"
)
return response
}
已通过身份验证的请求
app.get("user") { req async throws -> Response in
guard let session = req.cookies["a_session_[PROJECT_ID]"]?.string else {
throw Abort(.unauthorized)
}
let sessionClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
.setSession(session)
let account = Account(sessionClient)
let user = try await account.get()
// Return user as JSON
}
OAuth2 SSR 流程
// Step 1: Redirect to OAuth provider
app.get("oauth") { req async throws -> Response in
let account = Account(adminClient)
let redirectUrl = try await account.createOAuth2Token(
provider: .github,
success: "https://example.com/oauth/success",
failure: "https://example.com/oauth/failure"
)
return req.redirect(to: redirectUrl)
}
// Step 2: Handle callback — exchange token for session
app.get("oauth", "success") { req async throws -> Response in
let userId = try req.query.get(String.self, at: "userId")
let secret = try req.query.get(String.self, at: "secret")
let account = Account(adminClient)
let session = try await account.createSession(userId: userId, secret: secret)
let response = Response(status: .ok, body: .init(string: "{\"success\": true}"))
response.cookies["a_session_[PROJECT_ID]"] = HTTPCookies.Value(
string: session.secret,
isHTTPOnly: true, isSecure: true, sameSite: .strict, path: "/"
)
return response
}
Cookie 安全: 始终使用
isHTTPOnly、isSecure和sameSite: .strict来防止 XSS。Cookie 名称必须为a_session_<PROJECT_ID>。
转发用户代理: 调用
sessionClient.setForwardedUserAgent(req.headers.first(name: .userAgent) ?? ""),记录最终用户的浏览器信息,以便进行调试和安全检查。
错误处理
import Appwrite
// AppwriteException is included in the main module
do {
let row = try await tablesDB.getRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]")
} catch let error as AppwriteException {
print(error.message) // human-readable message
print(error.code) // HTTP status code (Int)
print(error.type) // error type (e.g. "document_not_found")
print(error.response) // full response body
}
常见错误代码:
| 代码 | 含义 |
|---|---|
401 | 未授权——缺少或无效的会话/API 密钥 |
403 | 禁止访问——权限不足 |
404 | 未找到——资源不存在 |
409 | 冲突——ID 重复或违反唯一性约束 |
429 | 超出速率限制——请求过多 |
权限与角色(重要)
Appwrite 使用权限字符串控制对资源的访问。每项权限将一个操作(read、update、delete、create,或授予 create + update + delete 权限的 write)与一个角色目标配对。默认情况下,除非在行/文件级别明确设置权限,或从表/存储桶设置继承权限,否则没有用户可以访问。权限是由 Permission 和 Role 辅助工具构建的字符串数组。
import Appwrite
// Permission and Role are included in the main module import
带权限的数据库行
let doc = try await tablesDB.createRow(
databaseId: "[DATABASE_ID]",
tableId: "[TABLE_ID]",
rowId: ID.unique(),
data: ["title": "Hello World"],
permissions: [
Permission.read(Role.user("[USER_ID]")), // specific user can read
Permission.update(Role.user("[USER_ID]")), // specific user can update
Permission.read(Role.team("[TEAM_ID]")), // all team members can read
Permission.read(Role.any()), // anyone (including guests) can read
]
)
带权限的文件上传
let file = try await storage.createFile(
bucketId: "[BUCKET_ID]",
fileId: ID.unique(),
file: InputFile.fromPath("/path/to/file.png"),
permissions: [
Permission.read(Role.any()),
Permission.update(Role.user("[USER_ID]")),
Permission.delete(Role.user("[USER_ID]")),
]
)
何时设置权限: 当需要对每个资源进行访问控制时,请设置行/文件级权限。如果表中的所有行都使用相同的规则,请在表/存储桶级别配置权限,并将行权限留空。
常见错误:
- 忘记设置权限 — 资源将对所有用户(包括创建者)不可访问
- 对
Role.any()使用write/update/delete— 允许任何用户(包括未通过身份验证的访客)修改或删除资源- 在敏感数据上使用
Permission.read(Role.any())— 使资源可被公开读取