Swift Package,用于服务器端和命令行访问 CloudKit Web 服务
这个 Swift 包使用的不是 CloudKit 框架,而是 CloudKit Web 服务。 为什么?
... 还有更多
在我的例子中,我正在将它用于我的 Apple Watch 应用程序 Heartwitch 的 Vapor 后端。这是一个示例代码,展示了如何使用 CloudKit 容器设置和使用 MistKit。
// Example for pulling a todo list from CloudKit
import MistKit
import MistKitNIOHTTP1Token
// setup your connection to CloudKit
let connection = MKDatabaseConnection(
container: "iCloud.com.brightdigit.MistDemo",
apiToken: "****",
environment: .development
)
// setup how to manager your user's web authentication token
let manager = MKTokenManager(storage: MKUserDefaultsStorage(), client: MKNIOHTTP1TokenClient())
// setup your database manager
let database = MKDatabase(
connection: connection,
tokenManager: manager
)
// create your request to CloudKit
let query = MKQuery(recordType: TodoListItem.self)
let request = FetchRecordQueryRequest(
database: .private,
query: FetchRecordQuery(query: query))
// handle the result
database.query(request) { result in
dump(result)
}
// wait for query here...
要同步等待 CloudKit 查询完成,您可以使用 CFRunLoop
...
// handle the result
database.query(request) { result in
dump(result)
// nessecary if you need run this synchronously
CFRunLoopStop(CFRunLoopGetMain())
}
// nessecary if you need run this synchronously
CFRunLoopRun()
以下是该库当前已实现的功能
Swift Package Manager 是 Apple 的去中心化依赖管理器,用于将库集成到您的 Swift 项目中。它现在与 Xcode 11 完全集成。
要使用 SPM 将 MistKit 集成到您的项目中,请在您的 Package.swift 文件中指定它
let package = Package(
...
dependencies: [
.package(url: "https://github.com/brightdigit/MistKit", from: "0.2.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["MistKit", ...]),
...
]
)
如果您正在构建服务器端实现,还有 SwiftNIO 和 Vapor 的产品
.target(
name: "YourTarget",
dependencies: ["MistKit",
.product(name: "MistKitNIO", package: "MistKit"), // if you are building a server-side application
.product(name: "MistKitVapor", package: "MistKit") // if you are building a Vapor application
...]
),
MistKit 需要使用以下属性设置连接
container 名称格式为 iCloud.com.*.*,例如 iCloud.com.brightdigit.MistDemoapiToken,可以通过 CloudKit 仪表板创建environment 可以是 development 或 production这是一个如何设置 MKDatabase 的示例
let connection = MKDatabaseConnection(
container: options.container,
apiToken: options.apiKey,
environment: options.environment)
// setup your database manager
let database = MKDatabase(
connection: connection,
tokenManager: manager
)
在进行实际请求之前,您应该了解如何为 private 或 shared 数据库进行身份验证的请求。
为了访问 private 或 shared 数据库,Cloud Web Services API 需要一个 Web 身份验证令牌。 为了让 MistKit 获取它,将设置一个 HTTP 服务器来侦听来自 CloudKit 的回调。
因此,当您设置 API 令牌时,请确保为 Sign-In Callback 设置一个 URL
设置好之后,您可以设置一个 MKTokenManager。
MKTokenManager 需要一个 MKTokenStorage 用于稍后存储令牌。 您可以使用以下几种实现
MKFileStorage 将令牌存储为简单的文本文件MKUserDefaultsStorage 使用 UserDefaults 存储令牌MKVaporModelStorage 通过 Fluent 将令牌存储在数据库 Model 对象中MKVaporSessionStorage 将令牌存储在 Vapor Session 数据中可选地,如果需要监听通过 MKTokenClient 进行的 Web 身份验证,MistKit 可以为您设置一个 Web 服务器:您可以使用以下几种实现
MKNIOHTTP1TokenClient 使用 SwiftNIO 设置 HTTP 服务器这是一个您如何设置 MKDatabase 的示例
let connection = MKDatabaseConnection(
container: options.container,
apiToken: options.apiKey,
environment: options.environment
)
// setup how to manager your user's web authentication token
let manager = MKTokenManager(
// store the token in UserDefaults
storage: MKUserDefaultsStorage(),
// setup an http server at localhost for port 7000
client: MKNIOHTTP1TokenClient(bindTo: .ipAddress(host: "127.0.0.1", port: 7000))
)
// setup your database manager
let database = MKDatabase(
connection: connection,
tokenManager: manager
)
如果您没有构建服务器端应用程序,您可以通过将 MistKitNIO 添加到您的包依赖项来使用 MKNIOHTTP1TokenClient
let package = Package(
...
dependencies: [
.package(url: "https://github.com/brightdigit/MistKit", .branch("main")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["MistKit", "MistKitNIOHTTP1Token", ...]),
...
]
)
当由于身份验证失败导致请求失败时,MKNIOHTTP1TokenClient 将启动一个 HTTP 服务器来开始侦听 Web 身份验证令牌。 默认情况下,MKNIOHTTP1TokenClient 将仅打印 URL,但您可以覆盖 onRequestURL
public class MKNIOHTTP1TokenClient: MKTokenClient {
public init(bindTo: BindTo, onRedirectURL : ((URL) -> Void)? = nil) {
self.bindTo = bindTo
self.onRedirectURL = onRedirectURL ?? {print($0)}
}
...
}
如果您可能已经有 webAuthenticationToken,您可以使用 MKStaticTokenManager。 这是一个 MKTokenManagerProtocol 的只读实现,它接受一个用于 webAuthenticationToken 的只读 String?。
这是我在 Vapor 应用程序 Heartwitch 中使用的一些示例代码,用于从我的数据库中提取 webAuthenticationToken 并在创建 MKDatabase 实例时使用该令牌。
import MistKit
import MistKitVapor
extension Application {
...
var cloudKitConnection: MKDatabaseConnection {
MKDatabaseConnection(
container: configuration.cloudkitContainer,
apiToken: configuration.cloudkitAPIKey,
environment: environment.cloudKitEnvironment
)
}
func cloudKitDatabase(using client: Client, withWebAuthenticationToken webAuthenticationToken: String? = nil) -> MKDatabase<MKVaporClient> {
MKDatabase(
connection: cloudKitConnection,
client: MKVaporClient(client: client),
tokenManager: MKStaticTokenManager(token: webAuthenticationToken, client: nil)
)
}
}
struct DeviceController {
func fetch(_ request: Request) throws -> EventLoopFuture<MKServerResponse<[DeviceResponseItem]>> {
let user = try request.auth.require(User.self)
let userID = try user.requireID()
let token = user.$appleUsers.query(on: request.db).field(\.$webAuthenticationToken).first().map { $0?.webAuthenticationToken }
let cloudKitDatabase: EventLoopFuture<MKDatabase> = token.map {
request.application.cloudKitDatabase(using: request.client, withWebAuthenticationToken: $0)
}
let cloudKitRequest = FetchRecordQueryRequest(
database: .private,
query: FetchRecordQuery(query: query)
)
let newEntries = cloudKitDatabase.flatMap {
let cloudKitResult = cloudKitDatabase.query(cloudKitRequest, on: request.eventLoop)
}
return newEntries.mistKitResponse()
}
...
}
除了静态字符串,您还可以将令牌存储在会话或数据库中。
在 mistdemod 演示 Vapor 应用程序中,有一个示例展示了如何基于使用 MKVaporModelStorage 和 MKVaporSessionStorage 的请求创建 MKDatabase
extension MKDatabase where HttpClient == MKVaporClient {
init(request: Request) {
let storage: MKTokenStorage
if let user = request.auth.get(User.self) {
storage = MKVaporModelStorage(model: user)
} else {
storage = MKVaporSessionStorage(session: request.session)
}
let manager = MKTokenManager(storage: storage, client: nil)
let options = MistDemoDefaultConfiguration(apiKey: request.application.cloudKitAPIKey)
let connection = MKDatabaseConnection(container: options.container, apiToken: options.apiKey, environment: options.environment)
// use the webAuthenticationToken which is passed
if let token = options.token {
manager.webAuthenticationToken = token
}
self.init(connection: connection, factory: nil, client: MKVaporClient(client: request.client), tokenManager: manager)
}
}
在这种情况下,对于 User 模型需要实现 MKModelStorable。
final class User: Model, Content {
...
@Field(key: "cloudKitToken")
var cloudKitToken: String?
}
extension User: MKModelStorable {
static var tokenKey: KeyPath<User, Field<String?>> = \User.$cloudKitToken
}
MKModelStorable 协议确保 Model 包含存储 Web 身份验证令牌所需的属性。
虽然命令行工具需要一个 MKTokenClient 来监听来自 CloudKit 的回调,但使用服务器端应用程序,您可以只添加一个 API 调用。 这是一个监听 ckWebAuthToken 并将其保存到 User 的示例
struct CloudKitController: RouteCollection {
func token(_ request: Request) -> EventLoopFuture<HTTPStatus> {
guard let token: String = request.query["ckWebAuthToken"] else {
return request.eventLoop.makeSucceededFuture(.notFound)
}
guard let user = request.auth.get(User.self) else {
request.cloudKitAPI.webAuthenticationToken = token
return request.eventLoop.makeSucceededFuture(.accepted)
}
user.cloudKitToken = token
return user.save(on: request.db).transform(to: .accepted)
}
func boot(routes: RoutesBuilder) throws {
routes.get(["token"], use: token)
}
}
如果您的应用程序已经使用了 Apple 现有的 CloudKit API,您还可以使用 CKFetchWebAuthTokenOperation 将 webAuthenticationToken 保存到您的数据库。
有两种方法来获取记录
MKAnyQuery 来获取 MKAnyRecord 项MKQueryRecord 的自定义类型要获取为 MKAnyRecord,只需使用匹配的 recordType(即 schema name)创建 MKAnyQuery。
// create your request to CloudKit
let query = MKAnyQuery(recordType: "TodoListItem")
let request = FetchRecordQueryRequest(
database: .private,
query: FetchRecordQuery(query: query)
)
// handle the result
database.perform(request: request) { result in
do {
try print(result.get().records.information)
} catch {
completed(error)
return
}
completed(nil)
}
这将为您提供包含具有您的值的 fields 属性的 MKAnyRecord 项
public struct MKAnyRecord: Codable {
public let recordType: String
public let recordName: UUID?
public let recordChangeTag: String?
public let fields: [String: MKValue]
...
MKValue 类型是一个枚举,其中包含字段的类型和值。
为了对请求使用自定义类型,您需要实现 MKQueryRecord。 这是一个包含 title 属性的 todo 项的示例
public class TodoListItem: MKQueryRecord {
// required property and methods for MKQueryRecord
public static var recordType: String = "TodoItem"
public static var desiredKeys: [String]? = ["title"]
public let recordName: UUID?
public let recordChangeTag: String?
public required init(record: MKAnyRecord) throws {
recordName = record.recordName
recordChangeTag = record.recordChangeTag
title = try record.string(fromKey: "title")
}
public var fields: [String: MKValue] {
return ["title": .string(title)]
}
// custom fields and methods to `TodoListItem`
public var title: String
public init(title: String) {
self.title = title
recordName = nil
recordChangeTag = nil
}
}
现在,您可以使用您的自定义类型创建一个 MKQuery。
// create your request to CloudKit
let query = MKQuery(recordType: TodoListItem.self)
let request = FetchRecordQueryRequest(
database: .private,
query: FetchRecordQuery(query: query)
)
// handle the result
database.query(request) { result in
do {
try print(result.get().information)
} catch {
completed(error)
return
}
completed(nil)
}
不要使用 MKDatabase.perform(request:),而使用 MKDatabase.query(_ query:),MKDatabase 会将值解码为您的自定义类型。
即将推出
let recordNames : [UUID] = [...]
let query = LookupRecordQuery(TodoListItem.self, recordNames: recordNames)
let request = LookupRecordQueryRequest(database: .private, query: query)
database.lookup(request) { result in
try? print(result.get().count)
}
即将推出
let request = GetCurrentUserIdentityRequest()
database.perform(request: request) { (result) in
try? print(result.get().userRecordName)
}
即将推出
let item = TodoListItem(title: title)
let operation = ModifyOperation(operationType: .create, record: item)
let query = ModifyRecordQuery(operations: [operation])
let request = ModifyRecordQueryRequest(database: .private, query: query)
database.perform(operations: request) { result in
do {
try print(result.get().updated.information)
} catch {
completed(error)
return
}
completed(nil)
}
为了删除和更新记录,您需要已经从 CloudKit 中获取了该对象。 因此,您需要运行 LookupRecordQueryRequest 或 FetchRecordQueryRequest 才能访问该记录。 访问记录后,只需使用您的记录创建一个删除操作
let query = LookupRecordQuery(TodoListItem.self, recordNames: recordNames)
let request = LookupRecordQueryRequest(database: .private, query: query)
database.lookup(request) { result in
let items: [TodoListItem]
do {
items = try result.get()
} catch {
completed(error)
return
}
let operations = items.map { (item) in
ModifyOperation(operationType: .delete, record: item)
}
let query = ModifyRecordQuery(operations: operations)
let request = ModifyRecordQueryRequest(database: .private, query: query)
database.perform(operations: request) { result in
do {
try print("Deleted \(result.get().deleted.count) items.")
} catch {
completed(error)
return
}
completed(nil)
}
}
与更新记录类似,您需要已经从 CloudKit 中获取了该对象。 同样,运行 LookupRecordQueryRequest 或 FetchRecordQueryRequest 才能访问该记录。 访问记录后,只需使用您的记录创建一个更新操作
let query = LookupRecordQuery(TodoListItem.self, recordNames: [recordName])
let request = LookupRecordQueryRequest(database: .private, query: query)
database.lookup(request) { result in
let items: [TodoListItem]
do {
items = try result.get()
} catch {
completed(error)
return
}
let operations = items.map { (item) -> ModifyOperation<TodoListItem> in
item.title = self.newTitle
return ModifyOperation(operationType: .update, record: item)
}
let query = ModifyRecordQuery(operations: operations)
let request = ModifyRecordQueryRequest(database: .private, query: query)
database.perform(operations: request) { result in
do {
try print("Updated \(result.get().updated.count) items.")
} catch {
completed(error)
return
}
completed(nil)
}
}
如果您正在构建服务器端应用程序并已经在使用 SwiftNIO,您可能想要利用一些助手,它们将已经使用现有模式和可用的 API。 主要来自 SwiftNIO 的 EventLoops 以及来自 SwiftNIO 和 Vapor 的相应 HTTP 客户端。
如果您正在 SwiftNIO (或 Vapor) 中构建服务器端应用程序,您可能正在使用 EventLoops 和 EventLoopFuture 进行异步编程。 EventLoopFutures 本质上是 SwiftNIO 的 Future/Promise 实现。 幸运的是,MistKit 中有一些助手方法提供 EventLoopFutures,类似于它们在 SwiftNIO 中实现的方式。 这些实现增强了已经存在的回调
public extension MKDatabase {
func query<RecordType>(
_ query: FetchRecordQueryRequest<MKQuery<RecordType>>,
on eventLoop: EventLoop
) -> EventLoopFuture<[RecordType]>
func perform<RecordType>(
operations: ModifyRecordQueryRequest<RecordType>,
on eventLoop: EventLoop
) -> EventLoopFuture<ModifiedRecordQueryResult<RecordType>>
func lookup<RecordType>(
_ lookup: LookupRecordQueryRequest<RecordType>,
on eventLoop: EventLoop
) -> EventLoopFuture<[RecordType]>
func perform<RequestType: MKRequest, ResponseType>(
request: RequestType,
on eventLoop: EventLoop
) -> EventLoopFuture<ResponseType> -> EventLoopFuture<ResponseType>
where RequestType.Response == ResponseType
}
此外,如果您使用结果作为 Vapor HTTP 响应的 Content,MistKit 提供了一个 MKServerResponse 枚举类型,它区分了身份验证失败(带有重定向 URL)和实际成功。
public enum MKServerResponse<Success>: Codable where Success: Codable {
public init(attemptRecoveryFrom error: Error) throws
case failure(URL)
case success(Success)
}
除了 EventLoopFuture,您还可以使用不同的 HTTP 客户端来调用 CloudKit Web 服务。
默认情况下,MistKit 使用 URLSession 通过 MKURLSessionClient 向 CloudKit Web 服务发出 HTTP 调用
public struct MKURLSessionClient: MKHttpClient {
public init(session: URLSession) {
self.session = session
}
public func request(withURL url: URL, data: Data?) -> MKURLRequest
}
但是,如果您正在使用 SwiftNIO 或 Vapor,使用它们的 HTTP 客户端进行这些调用更有意义
MKAsyncClient,它使用由 AsyncHTTPClient 库提供的 HTTPClientMKVaporClient,它使用由 Vapor 库提供的 Client在 mistdemod 示例中,您可以看到如何使用 Vapor Request 和 Request 的 client 属性创建 MKDatabase
extension MKDatabase where HttpClient == MKVaporClient {
init(request: Request) {
let manager: MKTokenManager
let connection : MKDatabaseConnection
self.init(
connection: connection,
factory: nil,
client: MKVaporClient(client: request.client),
tokenManager: manager
)
}
}
这里有两个关于如何通过 MistKit 在 CloudKit 中执行基本 CRUD 方法的示例
mistdemoc Swift package 可执行文件mistdemod 这里postMessage此代码根据 MIT 许可证分发。 有关更多信息,请参阅 LICENSE 文件。