Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CloudKit - CKQueryOperation with dependency

I'm just beginning working with CloudKit, so bear with me.

Background info

At WWDC 2015, apple gave a talk about CloudKit https://developer.apple.com/videos/wwdc/2015/?id=715

In this talk, they warn against creating chaining queries and instead recommend this tactic:

let firstFetch = CKFetchRecordsOperation(...)
let secondFetch = CKFetchRecordsOperation(...)
...
secondFetch.addDependency(firstFetch)

letQueue = NSOperationQueue()
queue.addOperations([firstFetch, secondFetch], waitUntilFinished: false)

Example structure

The test project database contains pets and their owners, it looks like this:

|Pets               |   |Owners     |
|-name              |   |-firstName |
|-birthdate         |   |-lastName  |
|-owner (Reference) |   |           |

My Question

I am trying to find all pets that belong to an owner, and I'm worried I'm creating the chain apple warns against. See below for two methods that do the same thing, but two ways. Which is more correct or are both wrong? I feel like I'm doing the same thing but just using completion blocks instead.

I'm confused about how to change otherSearchBtnClick: to use dependency. Where would I need to add

ownerQueryOp.addDependency(queryOp)

in otherSearchBtnClick:?

@IBAction func searchBtnClick(sender: AnyObject) {
    var petString = ""
    let container = CKContainer.defaultContainer()
    let publicDatabase = container.publicCloudDatabase
    let privateDatabase = container.privateCloudDatabase

    let predicate = NSPredicate(format: "lastName == '\(ownerLastNameTxt.text)'")
    let ckQuery = CKQuery(recordType: "Owner", predicate: predicate)
    publicDatabase.performQuery(ckQuery, inZoneWithID: nil) {
        record, error in
        if error != nil {
            println(error.localizedDescription)
        } else {
            if record != nil {
                for owner in record {
                    let myRecord = owner as! CKRecord
                    let myReference = CKReference(record: myRecord, action: CKReferenceAction.None)

                    let myPredicate = NSPredicate(format: "owner == %@", myReference)
                    let petQuery = CKQuery(recordType: "Pet", predicate: myPredicate)
                    publicDatabase.performQuery(petQuery, inZoneWithID: nil) {
                        record, error in
                        if error != nil {
                            println(error.localizedDescription)
                        } else {
                            if record != nil {
                                for pet in record {
                                    println(pet.objectForKey("name") as! String)

                                }

                            }
                        }
                    }
                }
            }
        }
    }
}

@IBAction func otherSearchBtnClick (sender: AnyObject) {
    let container = CKContainer.defaultContainer()
    let publicDatabase = container.publicCloudDatabase
    let privateDatabase = container.privateCloudDatabase

    let queue = NSOperationQueue()
    let petPredicate = NSPredicate(format: "lastName == '\(ownerLastNameTxt.text)'")
    let petQuery = CKQuery(recordType: "Owner", predicate: petPredicate)
    let queryOp = CKQueryOperation(query: petQuery)
    queryOp.recordFetchedBlock = { (record: CKRecord!) in
        println("recordFetchedBlock: \(record)")
        self.matchingOwners.append(record)
    }

    queryOp.queryCompletionBlock = { (cursor: CKQueryCursor!, error: NSError!) in
        if error != nil {
            println(error.localizedDescription)
        } else {
            println("queryCompletionBlock: \(cursor)")
            println("ALL RECORDS ARE: \(self.matchingOwners)")
            for owner in self.matchingOwners {
                let ownerReference = CKReference(record: owner, action: CKReferenceAction.None)
                let ownerPredicate = NSPredicate(format: "owner == %@", ownerReference)
                let ownerQuery = CKQuery(recordType: "Pet", predicate: ownerPredicate)
                let ownerQueryOp =  CKQueryOperation(query: ownerQuery)
                ownerQueryOp.recordFetchedBlock = { (record: CKRecord!) in
                    println("recordFetchedBlock (pet values): \(record)")
                    self.matchingPets.append(record)
                }
                ownerQueryOp.queryCompletionBlock = { (cursor: CKQueryCursor!, error: NSError!) in
                    if error != nil {
                        println(error.localizedDescription)
                    } else {
                        println("queryCompletionBlock (pet values)")
                        for pet in self.matchingPets {
                            println(pet.objectForKey("name") as! String)
                        }
                    }
                }
            publicDatabase.addOperation(ownerQueryOp)
            }
        }


    }
    publicDatabase.addOperation(queryOp)
}
like image 283
Charlie Avatar asked Sep 02 '15 14:09

Charlie


2 Answers

If you don't need cancellation and aren't bothered about retrying on a network error then I think you are fine chaining the queries.

I know I know, in WWDC 2015 Nihar Sharma recommended the add dependency approach but it would appear he just threw that in at the end without much thought. You see it isn't possible to retry a NSOperation because they are one-shot anyway, and he offered no example for cancelling operations already in the queue, or how to pass data from one operation from the next. Given these 3 complications that could take you weeks to solve, just stick with what you have working and wait for the next WWDC for their solution. Plus the whole point of blocks is to let you call inline methods and be able to access the params in the method above, so if you move to operations you kind of don't get full advantage of that benefit.

His main reason for not using chaining is the ridiculous one that he couldn't tell which error is for which request, he had names his errors someError then otherError etc. No one in their right mind names error params different inside blocks so just use the same name for all of them and then you know inside a block you are always using the right error. Thus he was the one that created his messy scenario and offered a solution for it, however the best solution is just don't create the messy scenario of multiple error param names in the first place!

With all that being said, in case you still want to try to use operation dependencies here is an example of how it could be done:

__block CKRecord* venueRecord;
CKRecordID* venueRecordID = [[CKRecordID alloc] initWithRecordName:@"4c31ee5416adc9282343c19c"];
CKFetchRecordsOperation* fetchVenue = [[CKFetchRecordsOperation alloc] initWithRecordIDs:@[venueRecordID]];
fetchVenue.database = [CKContainer defaultContainer].publicCloudDatabase;

// init a fetch for the category, it's just a placeholder just now to go in the operation queue and will be configured once we have the venue.
CKFetchRecordsOperation* fetchCategory = [[CKFetchRecordsOperation alloc] init];

[fetchVenue setFetchRecordsCompletionBlock:^(NSDictionary<CKRecordID *,CKRecord *> * _Nullable recordsByRecordID, NSError * _Nullable error) {
    venueRecord = recordsByRecordID.allValues.firstObject;
    CKReference* ref = [venueRecord valueForKey:@"category"];

    // configure the category fetch
    fetchCategory.recordIDs = @[ref.recordID];
    fetchCategory.database = [CKContainer defaultContainer].publicCloudDatabase;
}];

[fetchCategory setFetchRecordsCompletionBlock:^(NSDictionary<CKRecordID *,CKRecord *> * _Nullable recordsByRecordID, NSError * _Nullable error) {
    CKRecord* categoryRecord = recordsByRecordID.allValues.firstObject;

    // here we have a venue and a category so we could call a completion handler with both.
}];

NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[fetchCategory addDependency:fetchVenue];
[queue addOperations:@[fetchVenue, fetchCategory] waitUntilFinished:NO];

How it works is first it vetches a Venue record, then it fetches its Category.

Sorry there is no error handling but as you can see it was already a ton of code to do something can could be done in a couple of lines with chaining. And personally I find this result more convoluted and confusing than simply chaining together the convenience methods.

like image 113
malhal Avatar answered Nov 17 '22 16:11

malhal


in theory you could have multiple owners and therefore multiple dependencies. Also the inner queries will be created after the outer query is already executed. You will be too late to create a dependency. In your case it's probably easier to force the execution of the inner queries to a separate queue like this:

if record != nil {
    for owner in record {
        NSOperationQueue.mainQueue().addOperationWithBlock {

This way you will make sure that every inner query will be executed on a new queue and in the mean time that parent query can finish.

Something else: to make your code cleaner, it would be better if all the code inside the for loop was in a separate function with a CKReference as a parameter.

like image 33
Edwin Vermeer Avatar answered Nov 17 '22 16:11

Edwin Vermeer