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.MistDemo
apiToken
,可以通过 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
库提供的 HTTPClient
MKVaporClient
,它使用由 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 文件。