Swift CloudKit 和CKQuery:当queryResultBlock returns 查询游标时如何迭代检索记录
Swift CloudKit and CKQuery: how to iteratively retrieve records when queryResultBlock returns a query cursor
我正在使用 CloudKit 使用 CKQuery 从私有数据库检索记录,在异步函数中使用 CKQueryOperation.queryResultBlock。我已经找到了几个使用 queryCompletionBlock 的例子,但它已被弃用并被 queryResultBlock 取代,关于如何实现它的可用文档很少。只要不返回查询完成游标(<=100 条记录),我的函数就可以正常工作,但我不知道如何迭代它。
这是我使用的代码:
public func queryRecords(recordType: CKRecord.RecordType, predicate: NSPredicate) async throws -> [CKRecord] {
var resultRecords: [CKRecord] = []
let db = container.privateCloudDatabase
let query = CKQuery(recordType: recordType, predicate: predicate)
let operation = CKQueryOperation(query: query)
let operationQueue = OperationQueue() // for > 100 records
operationQueue.maxConcurrentOperationCount = 1 // for > 100 records
operation.zoneID = zoneID
debugPrint("query for recordType=\(recordType) in zone \(zoneID.zoneName) with predicate \(predicate)")
return try await withCheckedThrowingContinuation { continuation in
operation.queryResultBlock = { result in
switch result {
case .failure(let error):
debugPrint(error)
continuation.resume(throwing: error)
case .success(let ckquerycursor):
debugPrint("successful query completion after \(resultRecords.count) record(s) returned")
if let ckquerycursor = ckquerycursor {
debugPrint("***** received a query cursor, need to fetch another batch! *****")
let newOperation = CKQueryOperation(cursor: ckquerycursor) // for > 100 records
newOperation.queryResultBlock = operation.queryResultBlock // for > 100 records
newOperation.database = db // for > 100 records
operationQueue.addOperation(newOperation) // for > 100 records
}
continuation.resume(returning: resultRecords)
}
}
operation.recordMatchedBlock = { (recordID, result1) in
switch result1 {
case .failure(let error):
debugPrint(error)
case .success(let ckrecord):
resultRecords.append(ckrecord)
}
}
db.add(operation)
}
}
我试图从类似的示例中实现代码,但没有成功:上面的代码导致致命错误“SWIFT TASK CONTINUATION MISUSE”作为行
continuation.resume(returning: resultRecords)
显然被多次调用(非法)。用“// for > 100 records”注释的行代表我添加到迭代中的代码;其他一切都适用于 100 或更少的记录集。
我是否需要迭代调用 queryRecords 函数本身,传递查询游标(如果存在),或者是否可以像我在这里尝试的那样将迭代操作添加到队列中?
如果有人在使用 queryResultBlock(未弃用 queryCompletionBlock)之前做过此操作,请帮忙!
谢谢!
在 Swift 5.5 中不需要 queryResultBlock
。
我使用它是因为我的 CKRecord
类型总是与它们的 Swift 类型命名相同。如果需要,您可以将 recordType: "\(Record.self)"
替换为 recordType
。
public extension CKDatabase {
/// Request `CKRecord`s that correspond to a Swift type.
///
/// - Parameters:
/// - recordType: Its name has to be the same in your code, and in CloudKit.
/// - predicate: for the `CKQuery`
func records<Record>(
type _: Record.Type,
zoneID: CKRecordZone.ID? = nil,
predicate: NSPredicate = .init(value: true)
) async throws -> [CKRecord] {
try await withThrowingTaskGroup(of: [CKRecord].self) { group in
func process(
_ records: (
matchResults: [(CKRecord.ID, Result<CKRecord, Error>)],
queryCursor: CKQueryOperation.Cursor?
)
) async throws {
group.addTask {
try records.matchResults.map { try .get() }
}
if let cursor = records.queryCursor {
try await process(self.records(continuingMatchFrom: cursor))
}
}
try await process(
records(
matching: .init(
recordType: "\(Record.self)",
predicate: predicate
),
inZoneWith: zoneID
)
)
return try await group.reduce(into: [], +=)
}
}
}
这是我根据 Jessy 的回复修改的代码,我在其中添加了 CKRecordZone.ID 参数,以便将查询限制在特定的记录区域。该函数还允许从 public 或私有数据库查询,其中 public 数据库必须仅使用默认区域。
public func queryRecords(recordType: CKRecord.RecordType, predicate: NSPredicate, publicDB: Bool) async throws -> [CKRecord] {
let db = publicDB ? container.publicCloudDatabase : container.privateCloudDatabase
let zoneID = publicDB ? CKRecordZone.default().zoneID : zoneID
return try await db.records(type: recordType, predicate: predicate, zoneID: zoneID)
}
public extension CKDatabase {
/// Request `CKRecord`s that correspond to a Swift type.
///
/// - Parameters:
/// - recordType: Its name has to be the same in your code, and in CloudKit.
/// - predicate: for the `CKQuery`
func records(
type: CKRecord.RecordType,
predicate: NSPredicate = .init(value: true),
zoneID: CKRecordZone.ID
) async throws -> [CKRecord] {
try await withThrowingTaskGroup(of: [CKRecord].self) { group in
func process(
_ records: (
matchResults: [(CKRecord.ID, Result<CKRecord, Error>)],
queryCursor: CKQueryOperation.Cursor?
)
) async throws {
group.addTask {
try records.matchResults.map { try .get() }
}
if let cursor = records.queryCursor {
try await process(self.records(continuingMatchFrom: cursor))
}
}
try await process(
records(
matching: .init(
recordType: type,
predicate: predicate
),
inZoneWith: zoneID
)
)
return try await group.reduce(into: [], +=)
}
}
}
我正在使用 CloudKit 使用 CKQuery 从私有数据库检索记录,在异步函数中使用 CKQueryOperation.queryResultBlock。我已经找到了几个使用 queryCompletionBlock 的例子,但它已被弃用并被 queryResultBlock 取代,关于如何实现它的可用文档很少。只要不返回查询完成游标(<=100 条记录),我的函数就可以正常工作,但我不知道如何迭代它。
这是我使用的代码:
public func queryRecords(recordType: CKRecord.RecordType, predicate: NSPredicate) async throws -> [CKRecord] {
var resultRecords: [CKRecord] = []
let db = container.privateCloudDatabase
let query = CKQuery(recordType: recordType, predicate: predicate)
let operation = CKQueryOperation(query: query)
let operationQueue = OperationQueue() // for > 100 records
operationQueue.maxConcurrentOperationCount = 1 // for > 100 records
operation.zoneID = zoneID
debugPrint("query for recordType=\(recordType) in zone \(zoneID.zoneName) with predicate \(predicate)")
return try await withCheckedThrowingContinuation { continuation in
operation.queryResultBlock = { result in
switch result {
case .failure(let error):
debugPrint(error)
continuation.resume(throwing: error)
case .success(let ckquerycursor):
debugPrint("successful query completion after \(resultRecords.count) record(s) returned")
if let ckquerycursor = ckquerycursor {
debugPrint("***** received a query cursor, need to fetch another batch! *****")
let newOperation = CKQueryOperation(cursor: ckquerycursor) // for > 100 records
newOperation.queryResultBlock = operation.queryResultBlock // for > 100 records
newOperation.database = db // for > 100 records
operationQueue.addOperation(newOperation) // for > 100 records
}
continuation.resume(returning: resultRecords)
}
}
operation.recordMatchedBlock = { (recordID, result1) in
switch result1 {
case .failure(let error):
debugPrint(error)
case .success(let ckrecord):
resultRecords.append(ckrecord)
}
}
db.add(operation)
}
}
我试图从类似的示例中实现代码,但没有成功:上面的代码导致致命错误“SWIFT TASK CONTINUATION MISUSE”作为行
continuation.resume(returning: resultRecords)
显然被多次调用(非法)。用“// for > 100 records”注释的行代表我添加到迭代中的代码;其他一切都适用于 100 或更少的记录集。
我是否需要迭代调用 queryRecords 函数本身,传递查询游标(如果存在),或者是否可以像我在这里尝试的那样将迭代操作添加到队列中?
如果有人在使用 queryResultBlock(未弃用 queryCompletionBlock)之前做过此操作,请帮忙! 谢谢!
在 Swift 5.5 中不需要 queryResultBlock
。
我使用它是因为我的 CKRecord
类型总是与它们的 Swift 类型命名相同。如果需要,您可以将 recordType: "\(Record.self)"
替换为 recordType
。
public extension CKDatabase {
/// Request `CKRecord`s that correspond to a Swift type.
///
/// - Parameters:
/// - recordType: Its name has to be the same in your code, and in CloudKit.
/// - predicate: for the `CKQuery`
func records<Record>(
type _: Record.Type,
zoneID: CKRecordZone.ID? = nil,
predicate: NSPredicate = .init(value: true)
) async throws -> [CKRecord] {
try await withThrowingTaskGroup(of: [CKRecord].self) { group in
func process(
_ records: (
matchResults: [(CKRecord.ID, Result<CKRecord, Error>)],
queryCursor: CKQueryOperation.Cursor?
)
) async throws {
group.addTask {
try records.matchResults.map { try .get() }
}
if let cursor = records.queryCursor {
try await process(self.records(continuingMatchFrom: cursor))
}
}
try await process(
records(
matching: .init(
recordType: "\(Record.self)",
predicate: predicate
),
inZoneWith: zoneID
)
)
return try await group.reduce(into: [], +=)
}
}
}
这是我根据 Jessy 的回复修改的代码,我在其中添加了 CKRecordZone.ID 参数,以便将查询限制在特定的记录区域。该函数还允许从 public 或私有数据库查询,其中 public 数据库必须仅使用默认区域。
public func queryRecords(recordType: CKRecord.RecordType, predicate: NSPredicate, publicDB: Bool) async throws -> [CKRecord] {
let db = publicDB ? container.publicCloudDatabase : container.privateCloudDatabase
let zoneID = publicDB ? CKRecordZone.default().zoneID : zoneID
return try await db.records(type: recordType, predicate: predicate, zoneID: zoneID)
}
public extension CKDatabase {
/// Request `CKRecord`s that correspond to a Swift type.
///
/// - Parameters:
/// - recordType: Its name has to be the same in your code, and in CloudKit.
/// - predicate: for the `CKQuery`
func records(
type: CKRecord.RecordType,
predicate: NSPredicate = .init(value: true),
zoneID: CKRecordZone.ID
) async throws -> [CKRecord] {
try await withThrowingTaskGroup(of: [CKRecord].self) { group in
func process(
_ records: (
matchResults: [(CKRecord.ID, Result<CKRecord, Error>)],
queryCursor: CKQueryOperation.Cursor?
)
) async throws {
group.addTask {
try records.matchResults.map { try .get() }
}
if let cursor = records.queryCursor {
try await process(self.records(continuingMatchFrom: cursor))
}
}
try await process(
records(
matching: .init(
recordType: type,
predicate: predicate
),
inZoneWith: zoneID
)
)
return try await group.reduce(into: [], +=)
}
}
}