Swift CloudKit 和 CKQuery:当 queryResultBlock 返回查询游标时如何迭代检索记录

问题描述 投票:0回答:3

我正在使用 CloudKit 使用 CKQuery 从私有数据库检索记录,并在异步函数中使用 CKQueryOperation.queryResultBlock。我发现了几个使用 queryCompletionBlock 的示例,但它已被弃用并被 queryResultBlock 取代,关于如何实现它的可用文档很少。只要不返回查询完成游标,我的函数就可以很好地工作(<=100 records), but I'm unable to figure out how to iterate it.

这是我正在使用的代码:

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 个记录”注释的行代表我添加到迭代的代码;对于 100 或更少的记录集,其他一切都可以正常工作。

我是否需要迭代调用 queryRecords 函数本身,传递查询游标(如果存在),或者是否可以像我在此处尝试执行的那样将迭代操作添加到队列中?

如果有人在使用queryResultBlock(不是已弃用的queryCompletionBlock)之前做过此操作,请帮忙! 谢谢!

swift asynchronous cloudkit ckquery
3个回答
2
投票

在 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 $1.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: [], +=)
    }
  }
}

0
投票

这是我从 Jessy 的回复中修改的代码,我添加了 CKRecordZone.ID 参数,以便将查询限制到特定的记录区域。该函数还允许从公共或私有数据库进行查询,其中公共数据库必须仅使用默认区域。

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 $1.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: [], +=)
      }
    }
}

0
投票

我发现上面的解决方案效果很好,但对于大量记录(300+)来说需要太多时间。使用模拟器,第一次加载花了将近5分钟。第二次到第五次尝试每次都需要 17-18 秒。我认为原因是我的许多记录都包含小资产。由于我不太了解该示例,无法对其进行修改,因此我组合了一个不同的循环方法,该方法仅获取我需要的内容,但可以轻松用于数百条记录。此方法大约需要 2-3 秒才能获取相同的 300 多条记录。 不使用游标,而是按照修改日期的顺序以 100 条记录为批次获取记录,直到收到所有记录。

var startTime = 0 // test
var chkDone: [cabStruct] = []
for j in 0 ..< 50 { // max will be 50 batches of 100 = 5000 records
  // Only retrieve records after last time device was synched
  let chkA = try await fetchgetItems(xcloudSyncTime: startTime) // cloudSyncTime)
  chkDone.append(contentsOf: chkA)
  for i in 0 ..< chkA.count {
    // Determine new startTime
    let modTime = anyDateToInt(dateIn: chkA[i].modificationDate!)
    if modTime > startTime {
      startTime = modTime
    }
  }
  if chkA.count < 100 { // max record fetch has been reached
    break // all records have been fetched, end loop
    }
  } // fetch loop
  // Need to resort by recordName to avoid duplicates
  if chkDone.count > 0 {
    let chkSorted = chkDone.sorted {$0.recID.recordName < $1.recID.recordName}
    var chkNoDup: [cabStruct] = [chkSorted[0]]
    for i in 1 ..< chkSorted.count {
      if chkSorted[i].recID.recordName != chkSorted[i-1].recID.recordName {
      chkNoDup.append(chkSorted[i]) // keep only unique records
      }
    }  
  } 
private func fetchgetItems(xcloudSyncTime: Int) async throws ->[cabStruct] {
// Download only newly created records after last sync
// Ref: https://stackoverflow.com/questions/68292170/swift-continuation-doesnt-make-await-continue
let date = NSDate(timeIntervalSince1970: Double(xcloudSyncTime)) //
let pred = NSPredicate(format: "modificationDate > %@", date) // only new records
let sort = NSSortDescriptor(key: "modificationDate", ascending: true)
let query = CKQuery(recordType: "cabRecord", predicate: pred)
query.sortDescriptors = [sort]
let items: [cabStruct] = await withCheckedContinuation { continuation in
let op = CKQueryOperation(query: query)
op.desiredKeys = ["recNo", "recTime", "recString", "modificationDate"]
op.resultsLimit = 100 // limit of 400 ok also
op.recordMatchedBlock = { (recordId, result) in
switch result {
  case let .success(record):
    var cabRec = cabStruct()
    cabRec.recID = record.recordID
    cabRec.recTime = record["recTime"]
    cabRec.recNos = record["recNo"]
    cabRec.recString = record["recString"]
    cabRec.recordZone = record.recordID.zoneID.zoneName
    cabRec.modificationDate = record.modificationDate
    nAStruct.append(cabRec)
  case let .failure(error):
    actLog(newPrint: "getItems - something went wrong, error:" + error.localizedDescription)
  } // switch
 } // matchblock
 op.queryResultBlock = { result in
   continuation.resume(returning: [])
   } // result
 database.add(op)
 } // continuation
 return nAStruct
 } // end func
© www.soinside.com 2019 - 2024. All rights reserved.