Search code examples
iosuitableviewswiftcloudkit

Nested CloudKit queries and UITableView


I am trying to make a simple table-based app using CloudKit that allows you to drill down to find more information about a particular item. Let's say my app shows a directory of shops located inside theme parks, divided by sections of the park.

In my initial table view, I was able to get the list of ThemeParks. Tapping on a ThemePark drills down to another table view that has section header titles for each section of the park. I was able to query for the Sections that had a CKReference matching the chosen ThemePark. The problem occurs when I try to get the Shops that match each Section. I have no clue how to wait until the shops are all downloaded before reloading the table view. Here is some sample code from my view controller:

func getSectionsFromCloud() {
    let cloudContainer = CKContainer.defaultContainer()
    let publicDatabase = CKContainer.defaultContainer().publicCloudDatabase
    let recordToMatch = CKReference(recordID: themePark.recordID, action: CKReferenceAction.DeleteSelf)
    let predicate = NSPredicate(format: "themeParkOwner == %@", recordToMatch)
    let query = CKQuery(recordType: "ThemeParkSection", predicate: predicate)
    let sort = NSSortDescriptor(key: "name", ascending: true)
    query.sortDescriptors = [sort]
    self.publicDatabase.performQuery(query, inZoneWithID: nil, completionHandler: {
        results, error in

        if error == nil {
            println("Successfully retrieved sections")
            self.sections = results as [CKRecord]
            for section in self.sections {
                self.getShopsFromCloud(section)
            }

        } else {
            println(error)
        }
    })
}

And here is my getShopsFromCloud function:

func getShopsFromCloud(record:CKRecord) {
    let cloudContainer = CKContainer.defaultContainer()
    let publicDatabase = CKContainer.defaultContainer().publicCloudDatabase
    let recordToMatch = CKReference(recordID: record.recordID, action:CKReferenceAction.DeleteSelf)
    let predicate = NSPredicate(format: "sectionOwner == %@", recordToMatch)
    let query = CKQuery(recordType: "Shop", predicate: predicate)
    self.publicDatabase.performQuery(query, inZoneWithID: nil, completionHandler: {
            results, error in

        if error == nil {
            println("Successfully retrieved shops.")
            self.shops.append(results as [CKRecord])

        } else {
            println(error)
        }
    })
}

The problem is, getSectionsFromCloud() finishes before all of the getShopsFromCloud() calls. So if I reload the table view in the completionHandler for getSectionsFromCloud, it won't show the shops. However, if I reload the table view in the completionHandler for getShopsFromCloud, every time the query is executed (which is once for each section), numberOfRowsInSection also gets called once for each section. So if my numberOfRowsInSection is this:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        return self.shops[section].count
}

...I get a "fatal error: array index out of range" error, because the first array of shops are downloaded, but numberOfRowsInSection is then called for the second array, which isn't there yet. So how can I know when getShopsFromCloud() finishes and where in the world do I put that tableView.reloadData() call?


Solution

  • Try to use dependencies between fetch requests and add them to the Queue of the Database object (CKContainer.defaultContainer().publicDatabase().addOperation) instead of calling the convenience method