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.
Related
I have a tableview which acts as a newsfeed. The cells are filled from an array of newsfeed items. I get the JSON from the Server, create newsfeed items from that input and attach them to my newsfeed array. a newsfeed item contains a title, a description and an imageurl string.
At:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "ImageFeedItemTableViewCell1", for: indexPath) as! ImageFeedItemTableViewCell
var item = self.feed!.items[indexPath.row]
if (item.messageType == 1){
cell = tableView.dequeueReusableCell(withIdentifier: "ImageFeedItemTableViewCell1", for: indexPath) as! ImageFeedItemTableViewCell
cell.title.text = item.title
cell.description.text = item.contentText
if (item.imageURL as URL == URL(string: "noPicture")!)
{
cell.picture.image = UIImage(named:"empty")
}
else{
if (item.cachedImage == UIImage(named:"default-placeholder")){
let request = URLRequest(url: item.imageURL as URL)
cell.picture.image = item.cachedImage
cell.dataTask = self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
OperationQueue.main.addOperation({ () -> Void in
if error == nil && data != nil {
let image = UIImage(data: data!)
if (image != nil){
self.feed!.items[indexPath.row].cachedImage = image!
}
cell.picture.image = image
}
})
})
cell.dataTask?.resume()
}else
{
cell.picture.image = item.cachedImage
}
}
}
the cells from the rows get filled with my newsfeeditem data.
But since i keep all my newsfeeditems inside an array, the memory usage is high and gehts higher for each additional newsfeeditem. I want it to work with endless scrolling like twitter, so i wonder how experienced developers tackle this memory issue.
Your problem is in this lines or wherever you try to hold UIImage inside your array, this is really not advised and will cause crash due to memory since image is very large data and not advised to persist it in your RAM with UIImage inside array:
self.feed!.items[indexPath.row].cachedImage = image!
What you need to do is basically after fetch your image from URL, you save it to your app's documents folder and save the name or it's path that can distinct your image in cachedImage (just change the type to string or sth) and refetch it from your app's document folder when you need to show it in cellForRow
Flow: Fetch image -> save to disk and persist path in array -> refetch from disk with the path in cellForRow
I have added UITableView into UIScrollView, I have created an IBOutlet for height constraint of UITableView which helps me in setting the content size of UITableview.
I have 3 tabs and I switch tabs to reload data with different data source . Also the i have different custom cells when the tab changes.
So when the tab changes I call reloadData
here is my cellForRow function
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Configure the cell...
var cell:UITableViewCell!
let event:Event!
if(tableView == self.dataTableView)
{
let eventCell:EventTableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier, forIndexPath: indexPath) as! EventTableViewCell
eventCell.delegate = self
event = sectionsArray[indexPath.section].EventItems[indexPath.row]
eventCell.eventTitleLabel?.text = "\(event.title)"
eventCell.eventImageView?.image = UIImage(named: "def.png")
if let img = imageCache[event.imgUrl] {
eventCell.eventImageView?.image = img
}
else {
print("calling image of \(indexPath.row) \(event.imgUrl)")
// let escapedString = event.imgUrl.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
let session = NSURLSession.sharedSession()
do {
let encodedImageUrl = CommonEHUtils.urlEncodeString(event.imgUrl)
let urlObj = NSURL(string:encodedImageUrl)
if urlObj != nil {
let task = session.dataTaskWithURL(urlObj!, completionHandler: { ( data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
guard let realResponse = response as? NSHTTPURLResponse where
realResponse.statusCode == 200 else {
print("Not a 200 response, url = " + event.imgUrl)
return
}
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data!)
// Store the image in to our cache
self.imageCache[event.imgUrl] = image
// Update the cell
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate:EventTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? EventTableViewCell {
cellToUpdate.eventImageView?.image = image
}
})
}
})
task.resume()
}
} catch {
print("Cant fetch image \(event.imgUrl)")
}
}
cell = eventCell
}
else if(secodTabClicked)
{
let Cell2:cell2TableViewCell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier1, forIndexPath: indexPath) as! cell2TableViewCell
//Image loading again takes place here
cell = Cell2
}
else if(thirdTabClicked)
{
let Cell3:cell3TableViewCell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier2, forIndexPath: indexPath) as! cell3TableViewCell
//Image loading again takes place here
cell = Cell3
}
return cell
}
As you can see each tab has different custom cells with images.
Below are the problems I am facing
1) it takes time to reload data when I switch tabs and their is considerable lag time. On iphone 4s it is worse
2) When I open this page, first tab is selected by default, so when i scroll, everything works smoothly. But when i switch tabs, and when i scroll again after reloading of data, the scroll becomes jerky and immediately i get memory warning issue.
What I did so far?
1) I commented the image fetching code and checked whether that is causing jerky scroll, but its not.
2) I used time profiler, to check what is taking more time, and it points the "dequeueReusableCellWithIdentifier". So I dont know what is going wrong here.
Your code does not look "symmetric" with respect to cell set up when secodTabClicked and thirdTabClicked. I do not see firstTabClicked, and it looks to me that the condition that you are using to determine which tab is clicked overlaps with secodTabClicked and thirdTabClicked. In other words, you are probably getting into the top branch, and return EventTableViewCell when cell2TableViewCell or cell3TableViewCell are expected.
Refactoring your code to make type selection "symmetric" with respect to all three cell types should fix this problem.
Another solution could be making separate data sources for different tabs, and switching the data source instead of setting xyzTabClicked flags. You would end up with thee small functions in place of one big function, which should make your code easier to manage.
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)
}
}
I have a tableView that shows images in the cells, and I'm fetching the data in viewDidLoad function
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableview.dequeueReusableCellWithIdentifier("cardSelectionCell", forIndexPath: indexPath) as! MyCell
let card = fetchedResultsController.objectAtIndexPath(indexPath) as! Card
cell.Name?.text = card.name
var image: NSData = card.photo as NSData
cell.logo.image = UIImage(data: image)
let date = NSDate()
let formatter = NSDateFormatter()
formatter.timeStyle = .MediumStyle
cell.policyExpiryDate.text = formatter.stringFromDate(date)
return cell
}
But the problem is that when I start scrolling, the tableView is very laggy
so i tried to create a dictionary to convert the images in viewDidLoad()
var imageDictionary = [Card:UIImage]()
AllCards = context.executeFetchRequest(request, error: nil) as! [Card]
for card in AllCards {
imageDictionary[card] = UIImage(data: card.photo as NSData)
}
and in the tableView function:
cell.logo.image = imageDictionary[card]
but it still lagging
one more question: if one card is added to the core data, i have to fetch the Array of images again, so i tried to create the array in viewDidAppear() but the images does not appear in the first load and the tableView is still lagging.
First of all it depends really on the size of your image:
var image: NSData = card.photo as NSData
cell.logo.image = UIImage(data: image)
This lines can be very heavy. You can easily measure it like this for example:
let time1 = CACurrentMediaTime()
var image: NSData = card.photo as NSData
cell.logo.image = UIImage(data: image)
print(CACurrentMediaTime() - time1)
If it takes much time (for example more than 8 milliseconds) than you should optimize it. You can do it it many ways, for example, you can store UIImage references in some dictionary or array and then pass on displaying in cellForIndexPath:.
The other way is going to AsyncKit way - making your UI assync, load data in one thread and pass it to draw on main thread only. Now all you process are in main thread.
UPD: I think that the best approach in this case is to store pathes to images, then run in background process for UIImage resize and return preview. Also you can make cache for resized and not resized UIImage, the path or URL can be a key - so next time it will cost only to take UIImage from cache not to launch all the resize process again.
Also shoul notice that this operation:
cell.logo.image = UIImage(data: image)
create a system cache and can make big allocations in memory, because garbage collector won't kill it immediately, like in the case with UIWebView. So you can add your own autorelease pool like this around your function:
autoreleasepool { () -> () in
cell.logo.image = UIImage(data: image)
}
I was having this issue too, I solved it by using dispatch_async. This way the table cell will load before the image finishes loading.
let priority = DISPATCH_QUEUE_PRIORITY_HIGH
dispatch_async(dispatch_get_global_queue(priority, 0)) {
if let imgData = card.photo as? NSData {
let image = UIImage(data: imageData)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.logo.image = image
})
}
}
I am using parse to store and retrieve some data, which I then load into a UITableview, each cell contains some text and image, however when I open my tableview, any cells in the view do not show images until I scroll them out of view and back into view (I guess this is calling cellForRowAtIndexPath). Is there a way to check when all images are downloaded and then reload the tableview?
func loadData(){
self.data.removeAllObjects()
var query = PFQuery(className:"Tanks")
query.orderByAscending("createdAt")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
// The find succeeded.
for object in objects {
self.data.addObject(object)
}
} else {
// Log details of the failure
NSLog("Error: %# %#", error, error.userInfo!)
}
self.tableView.reloadData()
}
}
override func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell {
self.cell = tableView!.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath!) as TankTableViewCell // let cell:TankTableViewCell
let item:PFObject = self.data.objectAtIndex(indexPath!.row) as PFObject
self.cell.productName.alpha = 1.0
self.cell.companyName.alpha = 1.0
self.cell.reviewTv.alpha = 1.0
self.rating = item.objectForKey("rating") as NSNumber
cell.productName.text = item.objectForKey("prodName") as? String
cell.companyName.text = item.objectForKey("compName") as? String
self.cell.reviewTv.text = item.objectForKey("review") as? String
let userImageFile = item.objectForKey("image") as PFFile
userImageFile.getDataInBackgroundWithBlock({
(imageData: NSData!, error: NSError!) -> Void in
if (error == nil) {
let image = UIImage(data:imageData)
self.cell.productImage.image = image
}
}, progressBlock: {
(percentDone: CInt) -> Void in
if percentDone == 100{
}
})
self.setStars(self.rating)
// Configure the cell...
UIView.animateWithDuration(0.5, animations: {
self.cell.productName.alpha = 1.0
self.cell.companyName.alpha = 1.0
self.cell.reviewTv.alpha = 1.0
self.cell.reviewTv.scrollRangeToVisible(0)
})
return cell
}
The problem is that you use self.cell and that you change that reference each time a cell is returned. So, when the images are loaded they are all set into the last cell to be returned, which probably isn't on screen (or at least not fully).
Really you should be capturing the cell in the completion block of the image download (and checking that the cell is still linked to the same index path).
Also, you should cache the downloaded images so you don't always download the data / recreate the image.
You could set up a delegate method in your UITableViewController that gets called by another controller class that fetches the images. I doubt that's what you want to do though.
What you should do is initialize the cells with a default image, and have the cell controller itself go and fetch the image in the background, and update its UIImageView when the fetch completes. You definitely don't want to wait around for all images to load before reloading the table because a.) that takes a long time, and b.) what if one fails or times out?
Once the cell has loaded its image, if it is swapped out by the recycler and swapped back in, you can simply get the cached image by calling getData instead of getDataInBackground as long as isDataAvailable is true.
After your line:
self.cell.productImage.image = image
Try Adding:
cell.layoutSubviews() or self.cell.layoutSubviews()
It should render the subview, or your image in this case, on the first table view.