1

I have an app with a collectionview of images which are pulled from CloudKit. I have a CKManager class which performs all CK related methods. In the viewcontroller, I call a method in CKManager to retrieve the initial data from CK, which all works perfectly. I'm using CKQueryOperation so I can pull the data in blocks, although up until now I was setting the ckQueryOperation.resultsLimit = CKQueryOperationMaximumResults just for testing. As a result, when scrolling the collectionview, the images/cells do not "fade in" as you scroll. I assume this is because all of the data has been retrieved before rendering the cells. Currently there are about 50 records and it loads fairly quickly, but it definitely loads quicker when I set the results limit to say, 25.

My problem is that I don't fully understand how to do this using a cursor, even though I've already planned for it by implementing a cursor in my code. I found this thread which I understood for most part, but it's in Swift and it also doesn't answer all of my questions. I modified my code based of Edwin's answer in that thread, but I'm sure I'm missing something in the Swift to OB-C translation.

Below is the code I'm calling in my CKManager class. I can see from the logging that it is working correctly and recognizing the cursor. What I don't understand is how/when do I call it again to get the next block of results from that cursor point? If the resultsLimit is not set to maximum like it was initially, I get the amount of results specified (20) and it doesn't retrieve the remaining. So I don't know how to get the remaining results where the cursor left off. I do know that since I'm using collectionview, I would need to update the number of items in section each time I get the next block of results.

Thanks very much in advance!

UPDATED: Changed loadCloudKitDataWithCompletionHandler to add call to new method accepting a cursor - loadCloudKitDataFromCursor:withCompletionHandler:. Only thing missing is to figure out where in the ViewController to handle the results returned from the method with the cursor to update numberOfItemsInSection and then reload the CollectionView.

From CKManager...

- (void)loadCloudKitDataFromCursor:(CKQueryCursor *)cursor withCompletionHandler:(void (^)(NSArray *, CKQueryCursor *, NSError *))completionHandler {
    NSMutableArray *cursorResultSet = [[NSMutableArray alloc] init];
    __block NSArray *results;

    if (cursor) { // make sure we have a cursor to continue from
        NSLog(@"INFO: Preparing to load records from cursor...");
        CKQueryOperation *cursorOperation = [[CKQueryOperation alloc] initWithCursor:cursor];
        cursorOperation.resultsLimit = 20;

        // processes for each record returned
        cursorOperation.recordFetchedBlock = ^(CKRecord *record) {
            NSLog(@"RecordFetchBlock returned from cursor CID record: %@", record.recordID.recordName);
            [cursorResultSet addObject:record];
        };
        // query has completed
        cursorOperation.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error) {
            results = [cursorResultSet copy];
            [cursorResultSet removeAllObjects]; // get rid of the temp results array
            completionHandler(results, cursor, error);
            if (cursor) {
                NSLog(@"INFO: Calling self to fetch more data from cursor point...");
                [self loadCloudKitDataFromCursor:cursor withCompletionHandler:^(NSArray *results, CKQueryCursor *cursor, NSError *error) {
                    results = [cursorResultSet copy];
                    [cursorResultSet removeAllObjects]; // get rid of the temp results array
                    completionHandler(results, cursor, error);
                }];
            }
        };

        [self.publicDatabase addOperation:cursorOperation];
    }

}

- (void)loadCloudKitDataFromCursor:(CKQueryCursor *)cursor withCompletionHandler:(void (^)(NSArray *, CKQueryCursor *, NSError *))completionHandler {
    NSMutableArray *cursorResultSet = [[NSMutableArray alloc] init];
    __block NSArray *results;

    if (cursor) { // make sure we have a cursor to continue from
        NSLog(@"INFO: Preparing to load records from cursor...");
        CKQueryOperation *cursorOperation = [[CKQueryOperation alloc] initWithCursor:cursor];
        cursorOperation.resultsLimit = 20;

        // processes for each record returned
        cursorOperation.recordFetchedBlock = ^(CKRecord *record) {
            NSLog(@"RecordFetchBlock returned from cursor CID record: %@", record.recordID.recordName);
            [cursorResultSet addObject:record];
        };
        // query has completed
        cursorOperation.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error) {
            results = [cursorResultSet copy];
            [cursorResultSet removeAllObjects]; // get rid of the temp results array
            completionHandler(results, cursor, error);
            if (cursor) {
                NSLog(@"INFO: Calling self to fetch more data from cursor point...");
                [self loadCloudKitDataFromCursor:cursor withCompletionHandler:^(NSArray *results, CKQueryCursor *cursor, NSError *error) {
                    results = [cursorResultSet copy];
                    [cursorResultSet removeAllObjects]; // get rid of the temp results array
                    completionHandler(results, cursor, error);
                }];
            }
        };

        [self.publicDatabase addOperation:cursorOperation];
    }

}

From inside ViewController method which calls CKManager to get the data...

dispatch_async(queue, ^{
        [self.ckManager loadCloudKitDataWithCompletionHandler:^(NSArray *results, CKQueryCursor *cursor, NSError *error) {
            if (!error) {
                if ([results count] > 0) {
                    self.numberOfItemsInSection = [results count];
                    NSLog(@"INFO: Success querying the cloud for %lu results!!!", (unsigned long)[results count]);
                    [self loadRecipeDataFromCloudKit]; // fetch the recipe images from CloudKit
                    // parse the records in the results array
                    for (CKRecord *record in results) {
                        ImageData *imageData = [[ImageData alloc] init];
                        CKAsset *imageAsset = record[IMAGE];
                        imageData.imageURL = imageAsset.fileURL;
                        imageData.imageName = record[IMAGE_NAME];
                        imageData.imageDescription = record[IMAGE_DESCRIPTION];
                        imageData.userID = record[USER_ID];
                        imageData.imageBelongsToCurrentUser = [record[IMAGE_BELONGS_TO_USER] boolValue];
                        imageData.recipe = [record[RECIPE] boolValue];
                        imageData.liked = [record[LIKED] boolValue]; // 0 = No, 1 = Yes
                        imageData.recordID = record.recordID.recordName;
                        // check to see if the recordID of the current CID is userActivityDictionary. If so, it's in the user's private
                        // data so set liked value = YES
                        if ([self.imageLoadManager lookupRecordIDInUserData:imageData.recordID]) {
                            imageData.liked = YES;
                        }
                        // add the CID object to the array
                        [self.imageLoadManager.imageDataArray addObject:imageData];

                        // cache the image with the string representation of the absolute URL as the cache key
                        if (imageData.imageURL) { // make sure there's an image URL to cache
                            if (self.imageCache) {
                                [self.imageCache storeImage:[UIImage imageWithContentsOfFile:imageData.imageURL.path] forKey:imageData.imageURL.absoluteString toDisk:YES];
                            }
                        } else {
                            NSLog(@"WARN: CID imageURL is nil...cannot cache.");
                            dispatch_async(dispatch_get_main_queue(), ^{
                                //[self alertWithTitle:@"Yikes!" andMessage:@"There was an error trying to load the images from the Cloud. Please try again."];
                                UIAlertView *reloadAlert = [[UIAlertView alloc] initWithTitle:YIKES_TITLE message:ERROR_LOADING_CK_DATA_MSG delegate:nil cancelButtonTitle:CANCEL_BUTTON otherButtonTitles:TRY_AGAIN_BUTTON, nil];
                                reloadAlert.delegate = self;
                                [reloadAlert show];
                            });
                        }
                    }
                    // update the UI on the main queue
                    dispatch_async(dispatch_get_main_queue(), ^{
                        // enable buttons once data has loaded...
                        self.userBarButtonItem.enabled = YES;
                        self.cameraBarButton.enabled = YES;
                        self.reloadBarButton.enabled = YES;

                        if (self.userBarButtonSelected) {
                            self.userBarButtonSelected = !self.userBarButtonSelected;
                            [self.userBarButtonItem setImage:[UIImage imageNamed:USER_MALE_25]];
                        }
                        [self updateUI]; // reload the collectionview after getting all the data from CK
                    });
                }
                // load the keys to be used for cache look up
                [self getCIDCacheKeys];
            } else {
                NSLog(@"Error: there was an error fetching cloud data... %@", error.localizedDescription);
                dispatch_async(dispatch_get_main_queue(), ^{
                    //[self alertWithTitle:@"Yikes!" andMessage:@"There was an error trying to load the images from the Cloud. Please try again."];
                    UIAlertView *reloadAlert = [[UIAlertView alloc] initWithTitle:YIKES_TITLE message:ERROR_LOADING_CK_DATA_MSG delegate:nil cancelButtonTitle:CANCEL_BUTTON otherButtonTitles:TRY_AGAIN_BUTTON, nil];
                    reloadAlert.delegate = self;
                    [reloadAlert show];
                });
            }
        }];
    }
Community
  • 1
  • 1
SonnyB
  • 269
  • 3
  • 12

1 Answers1

1

You are close. For the newOperation you also have to set the recordFetchedBlock and queryCompletionBlock. When you assign the new operation to the operation and execute that, then you won't lose reference and your code will keep on running. Replace your one line of [self.publicDatabase addOperation:newOperation]; with:

newOperation.recordFetchedBlock = operation.recordFetchedBlock
newOperation.queryCompletionBlock = operation.queryCompletionBlock
operation = newOperation
[self.publicDatabase addOperation:operation];
Edwin Vermeer
  • 13,017
  • 2
  • 34
  • 58
  • Thanks again Edwin! You've helped me many times! However, there's a problem with your code. Where is "operation" coming from? It's not defined previously in your code or in mine. I thought maybe you means ckQueryOperation from my code above, but that didn't work either. Since I'm still a little in the dark on this, I can't modify it correctly on my own. Thanks! – SonnyB Jul 06 '15 at 13:40
  • Ah sorry, copy paste error. In your case it should be ckQueryOperation – Edwin Vermeer Jul 06 '15 at 16:10
  • I assumed that might have been what you mean so I tried that and it didn't work. Got a warning on newOperation.recordFetchedBlock = ckQueryOperation.recordFetchedBlock; (may lead to retain cycle) and an error on ckQueryOperation = newOperation; for not being assignable in the block. I resolved the error by adding __block to the ckQueryOperation declaration above, but not sure what I should do about the retain cycle warning. – SonnyB Jul 06 '15 at 16:32
  • Sorry, I only have Swift code for working with a cursor. In Swift the assignment of the newOperation to the to the operation was there to force the operation to be retained. Otherwise your code could be thrown away before the callback was executed. Not sure how to fix this in Objective C – Edwin Vermeer Jul 06 '15 at 18:57
  • I'll figure it out. Thanks! – SonnyB Jul 06 '15 at 19:49
  • I'm currently converting https://github.com/evermeer/EVCloudKitDao to Swift 2 and got an error for my code that handles the cursor. In my case the solution was instead of creating an operation in line i had to call en new function that created an entirely new CKQueryOperation. So I now have 2 functions that do almost the same. 1 that accepts a query and a 2nd that accepts a cursor. The first will call the 2nd and the 2nd will call itself with the next cursor – Edwin Vermeer Jul 07 '15 at 20:13
  • If you want to have a look at the Swift code, here it is: https://gist.github.com/evermeer/5df7ad1f8db529893f40 – Edwin Vermeer Jul 07 '15 at 20:16
  • I followed your advice and created a 2nd method in my CKManager class to accept a cursor which is called by the original method and it seems to be working. I can see via logging that I'm getting all the results in batches. The only thing I'm not sure of is how to update the VC once it's all processed? I updated my original post to include the updates to my CKManager methods and most of the method in the VC where it gets called initially to load this data. I need to update numberOfItemsInSection and reload the CV here, but where? How do I know when it's completed? Thanks!! – SonnyB Jul 10 '15 at 12:46
  • in both methods you have an 'if (cursor) {' then just add an else and there do a reloadData of your collectionView – Edwin Vermeer Jul 10 '15 at 14:32
  • You could also decide to reloadData after every call, also if there is still a cursor. Then you will also see the data coming in in batches. – Edwin Vermeer Jul 10 '15 at 14:34
  • Those two methods are in the CKManager (model/DAO) class. I was trying to figure out how to do exactly what you suggested, but in the VC method that makes the call to the CKManager method. – SonnyB Jul 10 '15 at 20:16
  • Just add a parameter to the 2 methods that take a callback function. Instead of doing the refresh call that callback function. Then when you call those functions put a reliaddata in that callback function. – Edwin Vermeer Jul 10 '15 at 20:19
  • I thought I was already doing that. Both of those methods already have a callback function with the completion handler. That's how I'm getting the results back to VC already. So I'm not sure what else I would add to those methods aside from what's already being returned. – SonnyB Jul 11 '15 at 11:49
  • Ah, I see there is already a completion handler, then in that hander you could do a reloadData isn't it? So first handle the data that is returned, and then do the reloadData – Edwin Vermeer Jul 11 '15 at 12:41
  • In the completion handler in the CKManager method? I don't have access to the CV in CKManager class to be able to do a reload. If you're talking about reloading in the VC, which I hope you are, that's my question...where in that method in the VC will I do a reload once those additional results have been passed? – SonnyB Jul 11 '15 at 12:56
  • No, from the VC you call the Manager while adding some code for the completion block. In that block which is in the VC you handle the completion of the CK call. You probably handle the data, then also to a CV reloadData. when you do so, make sure you are on the main queue. You could use something like NSOperationQueue.mainQueue().addOperationWithBlock { ...reloadData() } – Edwin Vermeer Jul 11 '15 at 15:05
  • In your manager you call completionHandler(results, cursor, error)... Its inside that handler where you can do the reloadData. If you still have a problem with it, then add the VC code that calls your manager to the question above. – Edwin Vermeer Jul 11 '15 at 15:06
  • Sorry, I had to leave for awhile....I'm still not seeing how to do the reload in the CK methods, but if you look above I had posted the code from the VC (inside the dispatch_async block) which calls the CK methods. And you're correct, in that VC method, I handle the data returned and reload (on the main queue). It could be that my code above is correct and that the issue is numberOfItemsInSection isn't getting updated in the VC to provide the correct number of cells to render. – SonnyB Jul 11 '15 at 18:20
  • it looks like if the method updateUI is called (maybe add a NSLog) that it should work. I see that you set the self.numberOfItemsInSection but don't you have a collectionview numberOfItemsInSection method? – Edwin Vermeer Jul 11 '15 at 19:49
  • I've always had an NSLog for when updateUI is called...all that does is reload the CV. And yeah, I have a numberOfItemsInSection method which returns self.numberOfItemsInSection property. – SonnyB Jul 11 '15 at 21:46
  • maybe add a log statement in the numberOfItemsInSection method where you log the number that you return. A reloadData should trigger that method. I'm starting to run out of clues... – Edwin Vermeer Jul 11 '15 at 21:49
  • I have that as well, which is how I'm able to tell that the numberOfItemsInSection is not right. I'll keep playing with it...I know I'm really close. Thanks! – SonnyB Jul 11 '15 at 21:53