3

Managed to cobble together a CKFetchRecordsOperation after much searching for sample code; and here it is... but I must have missed something. Don't get me wrong it works a treat... but...

To execute a CKFetchRecordsOperation you need an NSArray of CKRecordIDs; to get a NSArray of CKRecordIDs, you need to execute CKQuery thru which you can build your NSArray of CKRecordIDs.

But wait a minute, the process of extracting the CKRecordIDs uses a CKQuery, thru which I could simply download the CKRecords anyway?

How do you get your NSArray of CKRecordIDs if not with a CKQuery?

-(void)doSingleBeaconsDownload
{
    CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:@"iCloud.cqd.ch.BeaconBrowser"] publicCloudDatabase];
    NSPredicate *predicatex = [NSPredicate predicateWithFormat:@"iBeaconConfig = %@",iBeaconsConfirmed.giReferenceID];
    CKQuery *query = [[CKQuery alloc] initWithRecordType:@"Running" predicate:predicatex];

    [self.delegate performSelectorOnMainThread:@selector(processCompleted:) withObject:@"Downloading Configuration ..." waitUntilDone:YES];

    [publicDatabase performQuery:query inZoneWithID:nil completionHandler:^(NSArray *results, NSError *error) {

        if (error) {
            NSLog(@"Batch Download Error iCloud error %@",error);
        }
        else {
            NSMutableArray *rex2download = [[NSMutableArray alloc] init];
            for (CKRecord *rex in results) {
                [rex2download addObject:rex.recordID];
            }

            CKFetchRecordsOperation *fetchRecordsOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs:rex2download];

           /* fetchRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, CKRecordID *recordID, NSError *error) {
                if (error) {
                    // Retain the record IDs for failed fetches
                }
                else {
                   // Do something with each record downloaded
                }
            };*/

            fetchRecordsOperation.perRecordProgressBlock = ^(CKRecordID *record, double recordsDownloaded) {
                if (error) {
                    // damn ...
                } else {
                    NSLog(@"Downloaded %f", recordsDownloaded);
                }
            };
            fetchRecordsOperation.fetchRecordsCompletionBlock = ^(NSDictionary *recordsByRecordID, NSError *error) {
                if (error) {
                    // Failed to fetch all or some of the records
                }
                else {
                    for(CKRecord *record in results) {
                        NSLog(@"Fini download %lu",(unsigned long)[recordsByRecordID count]);
                    }
                    [self.delegate performSelectorOnMainThread:@selector(beaconsDownloaded:) withObject:noOf waitUntilDone:YES];
                }
            };
            [publicDatabase addOperation:fetchRecordsOperation];
        }
    }];
}
user3069232
  • 8,587
  • 7
  • 46
  • 87

3 Answers3

9

From Apple Documentation: A CKFetchRecordsOperation object retrieves CKRecord objects (whose IDs you already know) from iCloud.

A CKQueryOperation is used to retrieve CKRecords from iCloud based on some Query, so you can get them even if you do not know their recordIDs. A CKFetchRecordsOperation is used ONLY when you have the CKRecordIDs. You can create a CKRecordID without accessing iCloud, and you can store them in any local storage you have.

A good use case, which I use for this kind of operation, is when you want to modify a CKRecord, you need to first retrieve it from iCloud (using CKFetchRecordsOperation) and then save it back using CKModifyRecordsOperation.

Have a look at the two WWDC 2014 Videos on CloudKit that explain this pretty well.

harryhorn
  • 892
  • 6
  • 8
  • OK, I managed to craft an CKQueryOperation into the code, but it is still embedded. Tried implementing it with an NSOperation, but no way it seems to get it together with dependencies? Surely there is a more elegant way to link CKQuery/Fetch/Modify operations that nested loops... – user3069232 May 26 '15 at 16:16
0

Thanks for your help! I managed to craft an CKQueryOperation into the code, but ... but my code is soon going be become unreadable with many more of these nested loops? Surely there is a more elegant way to link CKQuery/Fetch/Modify operations; tried dependancies but missing something still ?

-(void)doSingleBeaconsDownload

{

[self.delegate performSelectorOnMainThread:@selector(processCompleted:) withObject:@"Downloading Configuration ..." waitUntilDone:YES];



CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:@"iCloud.cqd.ch.BeaconBrowser"] publicCloudDatabase];

NSPredicate *predicatex = [NSPredicate predicateWithFormat:@"iBeaconConfig = %@",iBeaconsConfirmed.giReferenceID];



CKQuery *query = [[CKQuery alloc] initWithRecordType:@"Running" predicate:predicatex];

CKQueryOperation *queryOp =[[CKQueryOperation alloc] initWithQuery:query];

queryOp.desiredKeys = @[@"record.recordID.recordName"];

queryOp.resultsLimit = CKQueryOperationMaximumResults;

NSMutableArray *rex2download = [[NSMutableArray alloc] init];



queryOp.recordFetchedBlock = ^(CKRecord *results)

{

   [rex2download addObject:results.recordID];

};



queryOp.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error)

{

// Cursor it seems contains a reference to a second call to it [required] if you try download more then 100 records       

    if (error) {

        NSLog(@"Batch Download Error iCloud error %@",error);

    }

    else {

        CKFetchRecordsOperation *fetchRecordsOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs:rex2download];

       fetchRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, CKRecordID *recordID, NSError *error) {

            if (error) {

                // Retain the record IDs for failed fetches

            }

            else {

              // Do something ..

            }

        };



        fetchRecordsOperation.perRecordProgressBlock = ^(CKRecordID *record, double recordsDownloaded) {

            if (error) {

                // damn

            } else {

                NSLog(@"Downloaded X");

            }

        };



        fetchRecordsOperation.fetchRecordsCompletionBlock = ^(NSDictionary *recordsByRecordID, NSError *error) {

            if (error) {

                // Failed to fetch all or some of the records

            }

            else {

                … Final clean up

            }

        };

        [publicDatabase addOperation:fetchRecordsOperation];

    }

 };



 [publicDatabase addOperation:queryOp];

}
user3069232
  • 8,587
  • 7
  • 46
  • 87
  • From a quick look you pretty much got it. Yes, their are a lot of blocks and inner blocks because you are dealing with asynchronous events. Can't be avoided. The bert way to deal with it is break each operation into a separate method and call the method from inside the success block. Also, if it is an option, swift makes for more readable and more easily maintained code with closures (compared to objective-c blocks) – harryhorn May 26 '15 at 18:48
  • Thanks Harry; Here is my third and final working solution; used a singleton global variable to pass the data between the two CKQueryOperations; don't know if that is best/good practice, but it works. ... seems a pity you cannot use something like this ... [fetchRecordsOperation addDependency:queryOp]; & [queue fetchRecordsOperation]; (doesn't compile) – user3069232 May 26 '15 at 19:57
  • OK. Please mark my answer as the correct answer if it is so :-) – harryhorn May 26 '15 at 21:47
0

Thanks Harry; Here is my third and final working solution; used a singleton global variable to pass the data between the two CKQueryOperations; don't know if that is best/good practice, but it works

... seems a pity you cannot use something like this ...

[fetchRecordsOperation addDependency:queryOp]; &
[queue fetchRecordsOperation]; (doesn't compile)

Would be a far cleaner solution... anyway here is V3 for completeness..

-(void)doSingleBeaconsDownloadV3
{

NSLog(@"doQuery executing");
CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:@"iCloud.cqd.ch.BeaconBrowser"] publicCloudDatabase];
NSPredicate *predicatex = [NSPredicate predicateWithFormat:@"iBeaconConfig = %@",iBeaconsConfirmed.giReferenceID];

CKQuery *query = [[CKQuery alloc] initWithRecordType:@"Running" predicate:predicatex];
CKQueryOperation *queryOp =[[CKQueryOperation alloc] initWithQuery:query];
queryOp.desiredKeys = @[@"record.recordID.recordName"];
queryOp.resultsLimit = CKQueryOperationMaximumResults;
//NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO];
//query.sortDescriptors = @[sortDescriptor];

queryOp.recordFetchedBlock = ^(CKRecord *results)
{
    [iBeaconsConfirmed.giRex2Download addObject:results.recordID];
    NSLog(@"fetched %lu",[iBeaconsConfirmed.giRex2Download count]);
};

queryOp.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error)
{
    if (error) {
        NSLog(@"Batch Download Error iCloud error %@",error);
    } else {
       NSLog(@"fetched %lu",[iBeaconsConfirmed.giRex2Download count]);
        [self DoFetchV2];
    }
};
[publicDatabase addOperation:queryOp];
}

-(void)DoFetchV2
{

NSLog(@"dofetch executing %lu",[iBeaconsConfirmed.giRex2Download count]);

CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:@"iCloud.cqd.ch.BeaconBrowser"] publicCloudDatabase];
CKFetchRecordsOperation *fetchRecordsOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs:iBeaconsConfirmed.giRex2Download];
fetchRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, CKRecordID *recordID, NSError *error) {
    if (error) {
        // Retain the record IDs for failed fetches
    }
    else {

        // Do something useful with data
    }
};

fetchRecordsOperation.perRecordProgressBlock = ^(CKRecordID *record, double recordsDownloaded)
{
    NSLog(@"Downloaded X");
};

fetchRecordsOperation.fetchRecordsCompletionBlock = ^(NSDictionary *recordsByRecordID, NSError *error) {
    if (error) {
        // Failed to fetch all or some of the records
    } else {
        NSLog(@"Fini download %lu",(unsigned long)[recordsByRecordID count]);
    }
};
 [publicDatabase addOperation:fetchRecordsOperation];

}

user3069232
  • 8,587
  • 7
  • 46
  • 87