0

Below I have my existing query download and cell for table row code...

 publicDB.perform(query, inZoneWith: nil)
    {
        (results, error) -> Void in
        if (error != nil)
        {
            self.present(alert, animated: true, completion: nil)
        }
        else
        {
            for result in results!
            {
                self.restaurantArray.append(result)
            }
            OperationQueue.main.addOperation( { () -> Void in
                self.tableView.reloadData()
            }) } }}
downloadRestaurants()
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
    let restaurant: CKRecord = restaurantArray[indexPath.row]
    cell?.name?.text = restaurant.value(forKey: "Name") as? String
    let asset = restaurant.value(forKey: "Picture") as! CKAsset 
    let data = try! Data(contentsOf: asset.fileURL)
   _ = UIImage(data: data)
    cell?.picture?.image = UIImage(data: data)
   return cell!
}

When I run this code, the app remains functional but scrolling through the 10 or so table cells is incredibly choppy. I am unsure what is causing this - all records, each containing an image, are downloaded during the query download portion of the top function. However, a problem or concept I'm missing is ever present during runtime. What am I missing here? Lazy loading? cache? something else? Unsure at this point, so any help would be incredibly helpful.

Update 1:

I've updated my code with a large thank you going to Pierce. I've had to update my code ever so slightly from his answer to maintain a ckrecord array to segue over to another controller via - restaurantArray but also create a new array for the NSObject class - tablerestaurantarray to be displayed in the current table controller.

   var restaurantArray: Array<CKRecord> = []
    var tablerestaurantarray: [Restaurant] = []

for result in results!
            {
                let tablerestaurant = Restaurant()

                if let name = result.value(forKey: "Name") as! String? {
                    tablerestaurant.name = name
                }
                // Do same for image
                if let imageAsset = result.object(forKey: "Picture") as! CKAsset? {

                    if let data = try? Data(contentsOf: imageAsset.fileURL) {
                        tablerestaurant.image =  UIImage(data: data)
                    }
                }
                self.tablerestaurantarray.append(tablerestaurant)

                self.restaurantArray.append(result)
            }
            OperationQueue.main.addOperation( { () -> Void in
                self.tableView.reloadData()     
            })
        }
    }
}
downloadRestaurants() 
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return restaurantArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell

    let restaurant: Restaurant = tablerestaurantarray[indexPath.row]
    cell?.name?.text = restaurant.name

    cell?.picture?.image = restaurant.image
    return cell!
}
Jon
  • 85
  • 1
  • 9
  • 1
    Every time you scroll in your tableView, your app has to convert a `CKAsset` into `Data` and then convert that to a `UIImage`. I would recommend creating some sort of `NSObject` called like `Restaurant` or something, then when you perform your query, take each record from the array of `CKRecords` and parse it into a `Restaurant` object that has a `UIImage` property, then use the images from those objects to populate the cells. That way it just has to grab a UIImage instead of converting it every time. – Pierce Feb 09 '17 at 00:00
  • Would you be able to supply a code sample, Pierce? I've never had to work with NSObjects before in my projects so I'm a tad bit behind on that one. – Jon Feb 09 '17 at 00:46
  • please see my answer below – Pierce Feb 09 '17 at 02:08
  • Pierce, I am incredibly thankful for your answer. Please take a look at my updated code. Question though... While the code works as intended, there is a slight slowdown during the first few initial scrolls. However, once I make it to the bottom of my table, everything is as snappy as hoped. Any idea? – Jon Feb 09 '17 at 04:35
  • That can be a very typical problem if you scroll through the first time when the image isn't already cached, then once you've made it to the bottom each image is cached. Check out this answer to see a solution http://stackoverflow.com/a/10818917/5378116 - NOTE: the answer is in Objective-C, but there is a link at the very top to a GitHub gist that has the same solution in Swift. Goodluck – Pierce Feb 09 '17 at 19:42
  • Pierce, I hate to reach out but this is the last dilemma in my project. Would you mind taking a look regarding a new cache question? http://stackoverflow.com/questions/42167023/creating-an-nscache-for-the-first-time-with-cloudkit-records-close – Jon Feb 10 '17 at 19:17

2 Answers2

1

The way your code is setup, whenever you scroll in your UITableView, your program is converting a CKAsset into Data, and then converting that into a UIImage, and that's within every cell! That's a rather inefficient process, so try creating an NSObject called something like Restaurant that has an image property, and when you go through all the records returned from your CKQuery, parse each record into a new Restaurant object. To create a new NSObject, go to File -> New -> File -> select 'Swift File' and add something like this:

import UIKit

class Restaurant: NSObject {

    // Create a UIImage property
    var image: UIImage?

    // Add any other properties, i.e. name, address, etc.
    var name: String = ""
}

Now for your query:

// Create an empty array of Restaurant objects
var restaurantArray: [Restaurant] = []

publicDB.perform(query, inZoneWith: nil) { (results, error) -> Void in
    if (error != nil) {
        self.present(alert, animated: true, completion: nil)
    } else {
        for result in results! {

            // Create a new instance of Restaurant
            let restaurant = Restaurant()

            // Use optional binding to check if value exists
            if let name = result.value(forKey: "Name") as! String? {
                restaurant.name = name
            }
            // Do same for image
            if let imageAsset = result.object(forKey: "Picture") as! CKAsset? {

                if let data = try? Data(contentsOf: imageAsset.fileURL) {
                    restaurant.image =  UIImage(data: data)
                }
            }

            // Append the new Restaurant to the Restaurants array (which is now an array of Restaurant objects, NOT CKRecords)
            self.restaurantArray.append(restaurant)
        }
        OperationQueue.main.addOperation( { () -> Void in
            self.tableView.reloadData()
        }) 
    } 
}

Now your cell setup is much simpler:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell

    let restaurant: Restaurant = restaurantArray[indexPath.row]
    cell?.name?.text = restaurant.name

    cell?.picture?.image = restaurant.image
    return cell!
}
Pierce
  • 3,148
  • 16
  • 38
0

You should use CKQueryOperation in order to implements pagination for your UITableView.

You have to set the resultLimit property to a number equals to the cell quantity visiable at one time on you table plus 3 or 4

Set recordFetchedBlock property where you have to implement the code that will apply to one CKRecord

Set queryCompletionBlock property. This is the most important part on your pagination code because this closure receive an Optional CKQueryCursor parameter.

If this CKQueryCursor is nil then you have reach the last record available for you query but if it's a non nil value, then you have more records to fetch using this CKQueryCursor as indicator to your next fetch.

When user scroll on your TableView and reach the last element you should perform another fetch with CKQueryCursor.

Other performance advice is CKAssets should be treated on separated execution queues.

Adolfo
  • 1,862
  • 13
  • 19