CloudKit: выборки всех записей с определенным типом записи?

В настоящее время у меня установлено CloudKit в моем приложении, поэтому я добавляю новую запись, используя следующий код ниже,

CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:@"stringArray"];
CKRecord *record = [[CKRecord alloc] initWithRecordType:@"Strings" recordID:recordID];
[record setObject:[NSArray arrayWithObjects:@"one", @"two", @"three", @"four", nil] forKey:@"stringArray"];
[_privateDatabase saveRecord:record completionHandler:nil];

Однако теперь я хотел бы получить ВСЕ записи, которые имеют один и тот же тип записи, "Строки", и возвращают скомпилированные в NSArray. Как мне это сделать? В настоящее время все, что я выяснил, - это то, как извлекать каждую запись по отдельности, используя идентификатор записи, что является проблемой, должен быть более простой способ.

[_privateDatabase fetchRecordWithID:recordID completionHandler:^(CKRecord *record, NSError *error) {
   if (error) {
      // Error handling for failed fetch from private database
   }
   else {
      NSLog(@"ICLOUD TEST: %@", [record objectForKey:@"stringArray"]);            
  }
}];

Ответ 1

Ааа, я понял. Используя приведенный ниже код, я смог создать запрос для запуска в базе данных, чтобы затем вернуть NSArray в блок завершения, через который я прошел цикл, и вернул значение для сохраненного ключа в NSLog.

NSPredicate *predicate = [NSPredicate predicateWithValue:YES];
CKQuery *query = [[CKQuery alloc] initWithRecordType:@"Strings" predicate:predicate];

[_privateDatabase performQuery:query inZoneWithID:nil completionHandler:^(NSArray *results, NSError *error) {
    for (CKRecord *record in results) {
        NSLog(@"Contents: %@", [record objectForKey:@"stringArray"]);
    }
}];

Ответ 2

Решение для Swift 4, показывает, как извлекать все записи типа "YourTable", также печатает System Field и Custom Field:

let query = CKQuery(recordType: "YourTable", predicate: NSPredicate(value: true))
CKContainer.default().publicCloudDatabase.perform(query, inZoneWith: nil) { (records, error) in
  records?.forEach({ (record) in

    // System Field from property
    let recordName_fromProperty = record.recordID.recordName
    print("System Field, recordName: \(recordName_fromProperty)")

    // Custom Field from key path (eg: deeplink)
    let deeplink = record.value(forKey: "deeplink")
    print("Custom Field, deeplink: \(deeplink ?? "")")
  })
}

Ответ 3

Вот ответ в Swift 3.0.

func myQuery()  {
    let predicate = NSPredicate(value: true)
    let query = CKQuery(recordType: "tableName", predicate: predicate)

    publicDatabase.perform(query, inZoneWith: nil) { (record, error) in

        for record: CKRecord in record! {
            //...

            // if you want to access a certain 'field'.
            let name = record.value(forKeyPath: "Name") as! String                
        }
    }
}

Ответ 4

Функция followig вернет ВСЕ записи для запрошенного типа записи:

let database = CKContainer(identifier: "container_here").privateCloudDatabase
typealias RecordsErrorHandler = ([CKRecord], Swift.Error?) -> Void

func fetchRecords(forType type: String, completion: RecordsErrorHandler? = nil) {

    var records = [CKRecord]()

    let query = CKQuery(recordType: type, predicate: NSPredicate(value: true))
    let queryOperation = CKQueryOperation(query: query)
    queryOperation.zoneID = CloudAssistant.shared.zone.zoneID

    queryOperation.recordFetchedBlock = { record in
        records.append(record)
    }

    queryOperation.queryCompletionBlock = { cursor, error in

        self.fetchRecords(with: cursor, error: error, records: records) { records in
            completion?(records, nil)
        }
    }

    database.add(queryOperation)
}

private func fetchRecords(with cursor: CKQueryCursor?, error: Swift.Error?, records: [CKRecord], completion: RecordsHandler?) {

    var currentRecords = records

    if let cursor = cursor, error == nil {

        let queryOperation = CKQueryOperation(cursor: cursor)

        queryOperation.recordFetchedBlock = { record in
            currentRecords.append(record)
        }

        queryOperation.queryCompletionBlock = { cursor, error in
            self.fetchRecords(with: cursor, error: error, records: currentRecords, completion: completion)
        }

        database.add(queryOperation)

    } else {
        completion?(records)
    }
}

Ответ 5

Пытаясь извлечь все записи и понять структуру и детали хранилища Cloudkit, я обнаружил, что во время отладки было полезно иметь следующую функцию. При этом используются семафоры для сохранения структуры данных для печати. Там может быть более элегантный способ сделать это, но это работает!

//
// Print a list of records in all zones in all databases
//
func printRecordsInContainers() {

    let myContainer = CKContainer.default()
    // Edit the following dictionary to include any known containers and possible record types
    let containerRecordTypes: [CKContainer: [String]] = [ myContainer: ["MyRecordType", "OldRecordType", "MyUser", "PrivateInfo"] ]
    let containers = Array(containerRecordTypes.keys)

    for containerz in containers {
        let databases: [CKDatabase] = [containerz.publicCloudDatabase, containerz.privateCloudDatabase, containerz.sharedCloudDatabase]

        for database in databases {
            var dbType = "<None>"
            if database.databaseScope.rawValue == 1 { dbType = "Public" }
            if database.databaseScope.rawValue == 2 { dbType = "Private" }
            if database.databaseScope.rawValue == 3 { dbType = "Shared" }

            //print("\(database.debugDescription)")
            print("\n\n\n🥨 ---------------------------------------------------------------------------------------------------")
            print("🥨 ----- Container: \(containerz.containerIdentifier ?? "??") ----- Database: \(dbType)")
            let semaphore1 = DispatchSemaphore(value: 0)     // Initiate semaphore1 to wait for closure to return

            database.fetchAllRecordZones { zones, error in
                if let error = error {
                    print("🧨 Error Fetching Zones: \(error.localizedDescription)")
                }
                else if let zones = zones {
                    print("~~~~ \(zones.count) : \(zones)")
                    for zone in zones {
                        print("----- Zone ID: \(zone.zoneID)\n")
                        for recordType in containerRecordTypes[container] ?? [] {
                            print("[  Record Type: \(recordType.description)  ]")
                            let query = CKQuery(recordType: recordType, predicate: NSPredicate(value: true))

                            let semaphore = DispatchSemaphore(value: 0)     // Initiate semaphore to wait for closure to return
                            database.perform(query, inZoneWith: zone.zoneID) { records, error in
                                if let error = error {
                                    print("🧨 Error in Record Query: \(error.localizedDescription)")
                                }
                                else if let records = records {
                                    printRecordDescriptions(records)
                                }
                                semaphore.signal()      // Send semaphore signal to indicate closure is complete
                            }
                            semaphore.wait()            // Wait for semaphore to indicate that closure is complete
                        }
                    }
                }
                else {
                    print("🧨 Error in fetchAllRecordZones")
                }
                semaphore1.signal()      // Send semaphore1 signal to indicate closure is complete
            }
            semaphore1.wait()            // Wait for semaphore1 to indicate that closure is complete
        }
    }
}
class func printRecordDescriptions(_ records: [CKRecord]) {
    print("Records and Contents List:")
    for record in records {
        print("🧰 Record: \(record.recordID)")
        for key in record.allKeys() {
            print("    Key - \(key)")
        }
    }
    print("Record List End\n")
}

Ответ 6

Swift 5

Изучив кучу сообщений и решений по SO, мне удалось найти решение, которое соответствует моим потребностям и должно быть достаточно простым для тех, кто просто хочет получить все свои записи данного типа из iCloud.

Решение

Решение, которое использует расширение CKDatabase для введения метода, который обрабатывает cursor: CKQueryOperation.Cursor из CKQueryOperation, чтобы продолжать запрашивать у iCloud больше записей. При таком подходе я отправляю в фоновую очередь, чтобы заблокировать ее и дождаться полного завершения операции, либо при получении ошибки, либо с последней партией записей. Как только семафор разблокирует очередь, он продолжает вызывать основной блок завершения с результатом. Также я использую преимущества типа Swift Result в обработчике завершения.

extension CKDatabase {

    func fetchAll(
        recordType: String, resultsLimit: Int = 100, timeout: TimeInterval = 60,
        completion: @escaping (Result<[CKRecord], Error>) -> Void
    ) {
        DispatchQueue.global().async { [unowned self] in
            let query = CKQuery(
                recordType: recordType, predicate: NSPredicate(value: true)
            )
            let semaphore = DispatchSemaphore(value: 0)
            var records = [CKRecord]()
            var error: Error?

            var operation = CKQueryOperation(query: query)
            operation.resultsLimit = resultsLimit
            operation.recordFetchedBlock = { records.append($0) }
            operation.queryCompletionBlock = { (cursor, err) in
                guard err == nil, let cursor = cursor else {
                    error = err
                    semaphore.signal()
                    return
                }
                let newOperation = CKQueryOperation(cursor: cursor)
                newOperation.resultsLimit = operation.resultsLimit
                newOperation.recordFetchedBlock = operation.recordFetchedBlock
                newOperation.queryCompletionBlock = operation.queryCompletionBlock
                operation = newOperation
                self?.add(newOperation)
            }
            self?.add(operation)

            _ = semaphore.wait(timeout: .now() + 60)

            if let error = error {
                completion(.failure(error))
            } else {
                completion(.success(records))
            }
        }
    }

}

Usage

UsageИспользование метода довольно просто для любого, кто знаком с синтаксисом закрытия Swift и типом Result.

let database: CKDatabase = ...
database.fetchAll(recordType: "User") { result in
    switch result {
        case .success(let users):
            // Handle fetched users, ex. save them to the database
        case .failure(let error):
            // Handle Error
        }
    }
}