Search code examples
iosobjective-cuicollectionviewcloudkit

Load UICollectionView with a cursor


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];
                });
            }
        }];
    }

Solution

  • 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];