Choppy scroll on UITableView displaying images from URL - ios

I have an issue with my table view being choppy on scroll while it loads each image inside the function of cellForItemAtIndexPath i've searched through some examples and these are the things i've tried and still have the same issue.
So i have this
var arrRes = [[String:AnyObject]]()
Then inside view did load i make an Alamofire GET request and with swiftyJSON i store the json file to the above dictionary.
if let resData = swiftyJsonVar["events"].arrayObject {
self.arrRes = resData as! [[String:AnyObject]]
}
self.tableview2.reloadData()
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrRes.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("mapCell", forIndexPath: indexPath) as! locationEventsTableViewCell
var dict = arrRes[indexPath.row]
cell.eventTitle.text = dict["eventName"] as? String
let cal = dict["eventStarttime"] as? String
let dateF = NSDateFormatter()
dateF.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
let date:NSDate = dateF.dateFromString(cal!)!
let d = NSDateFormatter()
d.dateFormat = "dd-MM-yyyy HH:mm"
let d1 = d.stringFromDate(date)
cell.eventDate.text = d1
let at = dict["eventStats"]?["attendingCount"] as? Int
let kapa = at?.stringValue
cell.eventAttends.text = kapa
let imageDef : UIImage = UIImage(named: "noimage")!
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
if let theImage = dict["eventCoverPicture"] as? String {
let url = NSURL(string: theImage)
if url != nil {
let data = NSData(contentsOfURL: url!)
dispatch_async(dispatch_get_main_queue()) {
cell.eventImage.image = UIImage(data: data!)
}
} else {
cell.eventImage.image = imageDef
}
}
}
return cell
}
So as you can see i am using the dispatch async function to get the image and even if i have it or not its still choppy.
Has anyone any solution about this? Thanks!

So the problem is that you're calling the images from a URL each time your UITableView is showing. Every time the cell goes off screen and comes back it's calling the method to retrieve the image from the server.
The server calls are being performed while the UI is trying to execute, this includes the scrolling and other visual loads.
Depending on the app, you can download all the images for the UITableView before you load the tableView and store them locally. I would also look into NSCache as that might be better for your app.
The goal is to have UI always be the number one priority. So if there are things that need to be in the UITableView like your eventCoverPicture, load them or call them from memory before you load the UITableView.
This ensures you're making the minimum amount of server calls necessary to reduce user network load.
The UI is interrupted and your users can scroll through their app without this choppiness.

I think your code is right about Async api. it's possibly the NSDateFormatter slowing you down. Date formatter is an heavy API. Use memorization, it would improve the prformance as well.
class MyObject {
// define static variable
private static let formatter: NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy"
return formatter
}()
// you could use it like so
func someMethod(date: NSDate) -> String {
return MyObject.formatter.stringFromDate(date)
}
}

Related

How to use Firestore query pagination with TableVIew

I am trying to use Firestore pagination with swift TableView. Here is my code which loads the first 4 posts from firestore.
func loadMessages(){
let postDocs = db
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.limit(to: 4)
postDocs.addSnapshotListener { [weak self](querySnapshot, error) in
self?.q.async{
self!.posts = []
guard let snapshot = querySnapshot else {
if let error = error {
print(error)
}
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
let nextDocs = Firestore.firestore()
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.start(afterDocument: lastSnapshot)
if let postsTemp = self?.createPost(snapshot){
DispatchQueue.main.async {
self!.posts = postsTemp
self!.tableView.reloadData()
}
}
}
}
}
func createPost(_ snapshot: QuerySnapshot) ->[Post]{
var postsTemp = [Post]()
for doc in snapshot.documents{
if let firstImage = doc.get(K.FStore.firstImageField) as? String,
let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
let secondImage = doc.get(K.FStore.secondImageField) as? String,
let secondTitle = doc.get(K.FStore.secondTitleField) as? String,
let userName = doc.get(K.FStore.poster) as? String,
let uID = doc.get(K.FStore.userID) as? String,
let postDate = doc.get("postTime") as? String,
let votesForLeft = doc.get("votesForLeft") as? Int,
let votesForRight = doc.get("votesForRight") as? Int,
let endDate = doc.get("endDate") as? Int{
let post = Post(firstImageUrl: firstImage,
secondImageUrl: secondImage,
firstTitle: firstTitle,
secondTitle: secondTitle,
poster: userName,
uid: uID,
postDate: postDate,
votesForLeft: votesForLeft,
votesForRight:votesForRight,
endDate: endDate)
postsTemp.insert(post, at: 0)
}else{
}
}
return postsTemp
}
Here is my delegate which also detects the end of the TableView:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
cell.delegate = self
let seconds = post.endDate
let date = NSDate(timeIntervalSince1970: Double(seconds))
let formatter = DateFormatter()
formatter.dateFormat = "M/d h:mm"
if(seconds <= Int(Date().timeIntervalSince1970)){
cell.timerLabel?.text = "Voting Done!"
}else{
cell.timerLabel?.text = formatter.string(from: date as Date)
}
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstTitle.setTitle(post.firstTitle, for: .normal)
cell.secondTitle.setTitle(post.secondTitle, for: .normal)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
cell.userName.setTitle(post.poster, for: .normal)
cell.firstImageView.layer.cornerRadius = 8.0
cell.secondImageView.layer.cornerRadius = 8.0
if(indexPath.row + 1 == posts.count){
print("Reached the end")
}
return cell
}
Previously I had an addSnapshotListener without a limit on the Query and just pulled down all posts as they came. However I would like to limit how many posts are being pulled down at a time. I do not know where I should be loading the data into my model. Previously it was being loaded at the end of the addSnapshotListener and I could still do that, but when do I use the next Query? Thank you for any help and please let me know if I can expand on my question any more.
There is a UITableViewDelegate method called tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) that will be called just before a cell is loading.
You could use this one to check if the row at IndexPath is in fact the cell of the last object in your tableview's datasource. Something like datasource.count - 1 == IndexPath.row (The -1 is to account for item 0 being the first item in an array, where as it already counts as 1).
If that object is indeed the last one in your datasource, you could make a call to Firebase and add items to the datasource. Before mutating the datasource, make sure to check the new number of objects the show (the ones already loaded + new ones) has to be larger than the current number of objects in the datasource, otherwise the app will crash.
You also might want to give your user a heads up that you're fetching data. You can trigger that heads up also in the delegate method.

Issue trying to complete Firebase Storage download before showing tableview

I have a table view where depending on the cell class it will download an image from Firebase. I've noticed when using the app that cells with the same cell identifier will show the previous downloaded image before showing the new one. This is what I have before changing it.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableData[indexPath.row]["Image"] != nil {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageNotesData", for: indexPath) as! ImageNotesCell
cell.notes.delegate = self
cell.notes.tag = indexPath.row
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
guard let imageFirebasePath = tableData[indexPath.row]["Image"] else {
return cell }
let pathReference = Storage.storage().reference(withPath: imageFirebasePath as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
cell.storedImage.image = image
}
}
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "notesData", for: indexPath) as! NotesCell
//let noteString = tableData[indexPath.row]["Notes"] as! String
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
cell.notes.delegate = self
cell.notes.tag = indexPath.row
return cell
}
}
Knowing that this is not a good user experience and that it looks clunky, I tried to move the pathReference.getData to where I setup the data but the view appears before my images finish downloading. I have tried to use a completion handler but I'm still having issues.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
getSectionData(userID: userID, city: selectedCity, completion: {(sectionString) in
self.setupTableCellView(userID: userID, city: selectedCity, section: sectionString) { (tableData) in
DispatchQueue.main.async(execute: {
self.cityName?.text = selectedCity
self.changeSections.setTitle(sectionString, for: .normal)
self.currentSectionString = sectionString
self.setupTableData(tableDataHolder: tableData)
})
}
})
}
func setupTableCellView(userID: String, city: String, section: String, completion: #escaping ([[String:Any]]) -> () ) {
let databaseRef = Database.database().reference().child("Users").child(userID).child("Cities").child(city).child(section)
var indexData = [String:Any]()
var indexDataArray = [[String:Any]]()
databaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
for dataSet in snapshot.children {
let snap = dataSet as! DataSnapshot
//let k = snap.key
let v = snap.value
indexData = [:]
for (key, value) in v as! [String: Any] {
//indexData[key] = value
if key == "Image" {
//let pathReference = Storage.storage().reference(withPath: value as! String)
print("before getImageData call")
self.getImageData(pathRef: value as! String, completion: {(someData) in
print("before assigning indexData[key]")
indexData[key] = someData
print("after assigning indexData[key]")
})
} else {
indexData[key] = value
}
}
indexDataArray.append(indexData)
}
completion(indexDataArray)
})
}
func getImageData(pathRef: String, completion: #escaping(UIImage) -> ()) {
let pathReference = Storage.storage().reference(withPath: pathRef as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614, completion: { (data, error) in
if let error = error {
print(error)
} else {
let image = UIImage(data:data!)
print("called before completion handler w/ image")
completion(image!)
}
})
}
I don't know if I am approaching this the right way but I think I am. I'm also guessing that the getData call is async and that is why it will always download after showing the table view.
You can't do this.
Make the request from Firebase.
Over time, you will get many replies - all the information and all the changing information.
When each new item arrives - and don't forget it may be either an addition or deletion - alter your table so that it displays all the current items.
That's OCC!
OCC is "occasionally connected computing". A similar phrase is "offline first computing". So, whenever you use any major service you use every day like Facebook, Snapchat, etc that is "OCC": everything stays in sync properly whether you do or don't have bandwidth. You know? The current major paradigm of device-cloud computing.
Edit - See Fattie's comments about prepareForReuse()!
With reusable table cells, the cells will at first have the appearance they do by default / on the xib. Once they're "used", they have whatever data they were set to. This can result in some wonky behavior. I discovered an issue where in my "default" case from my data, I didn't do anything ecause it already matched the xib, but if the data's attributes were different, I updated the appearance. The result was that scrolling up and down really fast, some things that should have had the default appearance had the changed appearance.
One basic solution to just not show the previous image would be to show a place holder / empty image, then call your asynchronous fetch of the image. Not exactly what you want because the cell will still show up empty...
Make sure you have a local store for the images, otherwise you're going to be making a server request for images you already have as you scroll up and down!
I'd recommend in your viewDidLoad, call a method to fetch all of your images at once, then, once you have them all, in your success handler, call self.tableview.reloadData() to display it all.

How to return a cell in tableview if it is nested inside an if statement

This is my code —- I am getting error when returning cell1 inside the if statement as it says ” Cannot return a non void return value in void function.I want to return the cell in tableview .. and i have 3 kind of posts .. one for status one for image one for video post. How can i return the cell for each.
P.S. : I have just provided the code for one post type only as if one is solved then all other can be solved.
import UIKit
import Alamofire
class ViewController: UIViewController , UITableViewDelegate ,
UITableViewDataSource{
#IBOutlet weak var feedTable: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
feedTable.dataSource = self
feedTable.delegate = self
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 376
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
Alamofire.request("https://new.postpfgsdfdsgshfghjoves.com/api/posts/get_all_posts").responseJSON { response in
let result = response.result
if let dict = result.value as? Dictionary<String,AnyObject> {
if let successcode = dict["STATUS_CODE"] as? Int {
if successcode == 1 {
if let postsArray = dict["posts"] as? [Dictionary<String,AnyObject>]
{
for i in 0..<postsArray.count
{
let posttype = postsArray[i]["media_type"] as! String
if posttype == "image"
{
let cell1 : ImageTableViewCell = self.feedTable.dequeueReusableCell(withIdentifier: "imageReuse") as! ImageTableViewCell
cell1.fullName = postsArray[i]["full_name"] as? String
cell1.profileImageURL = postsArray[i]["profile_pic"] as? String
cell1.location = postsArray[i]["location"] as? String
cell1.title = postsArray[i]["title"] as? String
cell1.postTime = postsArray[i]["order_by_date"] as? String
cell1.likes = postsArray[i]["liked_count"] as? Int
cell1.comments = postsArray[i]["comment_count"] as? Int
cell1.imageURL = postsArray[i]["profile_pic"] as? String
cell1.imageLocation = postsArray[i]["location"] as? String
cell1.content = postsArray[i]["content"] as? String
cell1.profileFullName.text = cell1.fullName
cell1.titleImagePost.text = cell1.title
cell1.postLocation.text = cell1.location
cell1.profileUserLocation.text = cell1.location
cell1.numberOfLikes.text = "\(cell1.likes!) Likes"
cell1.numberOfComments.text = "\(cell1.comments!) Comments"
cell1.postTimeOutlet.text = postsArray[i]["posted_on"] as? String
let url = URL(string: cell1.imageURL!)
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
cell1.profileImage.image = UIImage(data: data!)
let url1 = URL(string: cell1.imageURL!)
let data1 = try? Data(contentsOf: url1!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
cell1.postedImage.image = UIImage(data: data1!)
// return cell1
}
else if posttype == "status"
{
let cell1 : StatusTableViewCell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "statusReuse") as! StatusTableViewCell
cell1.fullName = postsArray[i]["full_name"] as? String
cell1.profileImageURL = postsArray[i]["profile_pic"] as? String
cell1.location = postsArray[i]["location"] as? String
cell1.title = postsArray[i]["title"] as? String
cell1.postTime = postsArray[i]["order_by_date"] as? String
cell1.likes = postsArray[i]["liked_count"] as? Int
cell1.comments = postsArray[i]["comment_count"] as? Int
cell1.postContent = postsArray[i]["content"] as? String
cell1.profileFullName.text = cell1.fullName
cell1.titleStatusPost.text = cell1.title
cell1.postLocation.text = cell1.location
cell1.profileUserLocation.text = cell1.location
cell1.content.text = cell1.postContent
cell1.numberOfLikes.text = "\(cell1.likes!) Likes"
cell1.numberOfComments.text = "\(cell1.comments!) Comments"
cell1.postTimeOutlet.text = "\(cell1.postTime!)"
let url = URL(string: cell1.profileImageURL!)
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
cell1.profileImage.image = UIImage(data: data!)
// return cell1
}
else if posttype == "video"
{
let cell1 : VideoTableViewCell = self.feedTable.dequeueReusableCell(withIdentifier: "videoReuse") as! VideoTableViewCell
cell1.fullName = postsArray[i]["full_name"] as? String
// cell1.profession = postsArray[i]["profession"] as? String
cell1.profileImageURL = postsArray[i]["profile_pic"] as? String
cell1.location = postsArray[i]["location"] as? String
cell1.title = postsArray[i]["title"] as? String
cell1.postTime = postsArray[i]["order_by_date"] as? String
cell1.likes = postsArray[i]["liked_count"] as? Int
cell1.comments = postsArray[i]["comment_count"] as? Int
cell1.videoURL = postsArray[i]["profile_pic"] as? String
cell1.profileFullName.text = cell1.fullName
cell1.titleVideoPost.text = cell1.title
cell1.postLocation.text = cell1.location
cell1.profileUserLocation.text = cell1.location
// return cell1
}
}
}
}
}
}
}
}
}
My answer isn't any different from the others but let me be a little more specific. I'll use a generic example and you'll need to tailor this to your specific needs.
1) Define a model somewhere for your data such as:
class MyDataItem {
var name: String
var title: String
var location: String
init(name: String, title: String, location: String) {
self.name = name
self.title = title
self.location = location
}
}
2) Define an array in your Viewcontroller such as:
var dataArray = [MyDataItem]()
3) Load the data which you could do from the viewDidLoad method:
override func viewDidLoad() {
super.viewDidLoad()
feedTable.dataSource = self
feedTable.delegate = self
loadData()
}
4) Implement loadData() function:
func loadData() {
// Here put in your alamo enclosure to retrieve the data and store it into the array you've defined
// When done, call reload data
feedTable.reloadData()
}
5) Your cellForRowAt function will need to be modified to retrieve the data from the array. For example:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell1 : ImageTableViewCell = tableView.dequeueReusableCell(withIdentifier: "imageReuse") as! ImageTableViewCell
cell1.fullName = dataArray[indexPath.row].name
cell1.title = dataArray[indexPath.row].title
cell1.location = dataArray[indexPath.row].location
return cell1
}
Anyway, this is the general idea on how to do what you are attempting. When reloadData is called from your loadData function, it will cause the tableview to reload from the array data correctly.
Hope this helps!
The problem is you do not return the cell, you simply make some async request with alamofire and return an instance of the cell from the closure.
func foo() -> Int { return 1 } ≠ func bar() -> Int { someClosure { return 1 } }
Firstly you need load the the data from https://www.example.com/api/posts/get_all_posts into some data model.
var models: [SomeTypeYouCreate] = []
func loadData() {
Alamofire.request(...).responseJSON { response in
self.models = /* Create array of `SomeTypeYouCreate` objects from response */
self.tableView.reloadData()
}
}
func func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = self.models[indexPath.row]
// configure cell with model
return cell
}
You cannot do it the way you're trying to. You're not returning a cell from cellForRowAt method, you're returning it in Alamofire callback closure. What you should do is to return the cell in your cellForRowAt method, and implement some sort of setup method for your UITableViewCell subclass and make your calls in there
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell1 : ImageTableViewCell = self.feedTable.dequeueReusableCell(withIdentifier: "imageReuse") as! ImageTableViewCell
// put your Alamofire code inside such function in your UITableViewCell subclass
cell.setup()
return cell
}
First and foremost, you are returning value in closure Alamofire.request. If you wanna use cell after you confirm cell values, you want to pass over completion handler to the function and use it in that Alamofire.reqeust...
But if I were you, I would create another function which is called before/after tableView function.
If it is Before then trigger tableview initialization upon alamofire completion.
If it is After then reload when values are loaded correctly in Alamofire.
EDITED:
Like other suggested,it is bad idea to load data in tableView function. Also, by using Alamofire, it means you use Closure. That is, whatever you wanna do in Alamofire happens asynchronously, meaning by the time what you want to achieve in Alamofire is done, your program can be out of the table view function. Also, since it is closure, returning value in Alamofire does not satisfy your tableView return type.
So basically, if you need data via API and verify, you declare function such that do whatever you doing Alamofire and then reload the tableView.
So flow is like this:
1) Make an empty array and put array.count to # of rows.
2) Since it is empty, when tableView first try to generate cells, it doesn't do anything.
3) You call the function which uses Alamofire. If returned values are good, then add the cell(model) to the array.
4) After you are done loading models, do tableView.reload().
5) Tableview calls tableView function now it finds value in array so that will create cells.

Unable to get value of Parse.com column

This is the craziest error I've encountered ever. I am trying to get the value from a column called nameRetailer in the Orders table, but it keeps getting nil.
Other columns of same String type are returning properly including the status column shown below.
What could lead to this? The spelling is certainly correct. Please help....
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let date = object?.objectForKey("dueDate") as! NSDate
let strDate = dateFormatter.stringFromDate(date)
cell.retailerName.text = object?.objectForKey("nameRetailer") as? String
cell.orderDueDate.text = strDate
cell.orderStatus.text = object?.objectForKey("status") as? String
When I tried to print the value of object?.objectForKey("nameRetailer"), it shows nil in console. In the parse data browser, column has data and was refreshed.
Update: Adding additional code:
The entire class code responsible for the table view:
class OrderViewController: PFQueryTableViewController {
override func queryForTable() -> PFQuery {
let query = PFQuery(className: "Orders")
query.cachePolicy = .CacheElseNetwork
query.orderByAscending("createdAt")
return query
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! OrdersTableViewCell
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let date = object?.objectForKey("dueDate") as! NSDate
let strDate = dateFormatter.stringFromDate(date)
cell.retailerName.text = object?.objectForKey("nameRetailer") as? String
cell.orderDueDate.text = strDate
cell.orderStatus.text = object?.objectForKey("status") as? String
print(object)
//let imageFile = object?.objectForKey("image") as PFFile
//cell.cellImageView.image = UIImage(named:"placeholder")
//cell.cellImageView.file = imageFile
//cell.cellImageView.loadInBackground()
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row + 1 > self.objects?.count
{
return 44
}
let height = super.tableView(tableView, heightForRowAtIndexPath: indexPath)
return height
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row + 1 > self.objects?.count
{
self.loadNextPage()
tableView.deselectRowAtIndexPath(indexPath, animated: true)
self.performSegueWithIdentifier("showDetail", sender: self)
}
}
An image of the table of orders:
and here is the log snapshot of printing PFObject:
And here is the updated snapshots showing the two rows
You have set your cache policy to start by looking up data in the local cache, which may be stale.
Change:
query.cachePolicy = .CacheElseNetwork
to
query.cachePolicy = .NetworkOnly // ignores cache on reading, but saves to cache
or
query.cachePolicy = .IgnoreCache // no cache at all -- this is the default
(or other appropriate value based on your specific context and use case)

Loading images in the background using CloudKit?

I'm working on an app in Swift that uses a UITableViewController with a fixed row height of 200. I have JPG images stored on CloudKit in a native resolution of 320 x 200. They range from around 25k to 45k apiece.
The issues I'm having is when the table populates, it loads up the 2-3 cells on the screen with the images and text, as it should. I'd like the rest of the cells to load in the background, since there are only a total of 12 entries. In the future there might be as many as 25-30.
As it's working now, when I begin scrolling, the proper text loads into the cells but the images lag a little and load in over the top of a previously display image.
From doing some reading, is this the result of the reusable cell? Is there a way to load all the cells in the background so it doesn't load the images only after I scroll?
Here's the code I've got:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return traders.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! CustomTableViewCell
if traders.isEmpty {
// come back here later
return cell
}
let trader = traders[indexPath.row]
if let traderDate = trader.objectForKey("modificationDate") as? NSDate {
//format date
var dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "EEE MMM dd hh:mm a"
var dateString = dateFormatter.stringFromDate(traderDate)
cell.dateLabel?.text = dateString
}
cell.nameLabel?.text = trader.objectForKey("StringAttribute") as? String
cell.placeLabel?.text = trader.objectForKey("Place") as? String
println(cell.placeLabel.text)
// Can we can get the image from cache?
if let imageFileURL = imageCache.objectForKey(trader.recordID) as? NSURL {
println("Got image from cache")
cell.traderImageView?.image = UIImage(data: NSData(contentsOfURL: imageFileURL)!)
} else {
// OK then, fetch the image from CloudKit in the background
let publicDatabase = CKContainer.defaultContainer().publicCloudDatabase
let fetchRecordsImageOperation = CKFetchRecordsOperation(recordIDs: [trader.recordID])
fetchRecordsImageOperation.desiredKeys = ["image", "detailImage"]
fetchRecordsImageOperation.queuePriority = .VeryHigh
fetchRecordsImageOperation.perRecordCompletionBlock = {(record:CKRecord!, recordID:CKRecordID!, error:NSError!) -> Void in
if (error != nil) {
println("Failed to get image: \(error.localizedDescription)")
} else {
if let traderRecord = record {
dispatch_async(dispatch_get_main_queue(), {
if let imageAsset = traderRecord.objectForKey("image") as? CKAsset {
cell.traderImageView?.image = UIImage(data: NSData(contentsOfURL: imageAsset.fileURL)!)
self.imageCache.setObject(imageAsset.fileURL, forKey: trader.recordID)
}
})
}
}
}
println("add operation")
publicDatabase.addOperation(fetchRecordsImageOperation)
}
println("return cell")
return cell
}
I'm assuming there must be a way to accomplish what I'm after, and pre-load all the cells with the images. I've contemplated storing the images as assets in the code itself (which I assume would load faster), but I really want access to change the images through the CloudKit dashboard without having to resubmit a new app each time I want to change an image.
You should look at the caching libraries, such as SDWebImage or FastImageCache. The lag appears due to several reasons - jpg decoding, loading from disk, some memory issues with Core Animation when the image is not aligned in memory.
I prefer to use FastImageCache, because it is awesome and boosts image loading incredibly. This benchmark shows a little advantage of it in case when image is already downloaded and persists on disk.

Resources