UITableView is Lagging when Displaying Images Swift - ios

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
})
}
}

Related

How to Flush Cache to Free up Memory when using UIImage - Swift 3.0

After reading a number of answers on SO and other articles (see below) what is the best method to manage memory when you are loading multiple images into an animation array?
http://www.alexcurylo.com/2009/01/13/imagenamed-is-evil/
Here are my goals:
Create Animation Array (see code below)
After the Animation plays flush the cache and load another animation (rinse, wash, repeat, you get it :)
As Apple states
https://forums.developer.apple.com/thread/17888
Using the UIImage(named: imageName) caches the images, but in my case after playing 2-3 animations in a row iOS terminates the App (the OS does not respond to a low memory warning and instead terminates the App - see end of code below)
I don't want to cache the images and rather we could either:
Flush the memory each time an animation completes and then load a new animation; or
Flush the memory when the user moves to a new Scene
Here is my code:
// create the Animation Array for each animation
func createImageArray(total: Int, imagePrefix: String) -> [UIImage]{
var imageArray: [UIImage] = []
for imageCount in 0..<total {
let imageName = "\(imagePrefix)-\(imageCount).png"
let image = UIImage(named: imageName)! // here is where we need to address memory and not cache the images
//let image = UIImage(contentsOfFile: imageName)! // maybe this?
imageArray.append(image)
}
return imageArray
}
// here we set the animate function values
func animate(imageView: UIImageView, images: [UIImage]){
imageView.animationImages = images
imageView.animationDuration = 1.5
imageView.animationRepeatCount = 1
imageView.startAnimating()
}
// Here we call the animate function
animation1 = createImageArray(total: 28, imagePrefix: "ImageSet1")
animation2 = createImageArray(total: 53, imagePrefix: "ImageSet2")
animation3 = createImageArray(total: 25, imagePrefix: "ImageSet3")
func showAnimation() {
UIView.animate(withDuration: 1, animations: {
animate(imageView: self.animationView, images: self.animation1)
}, completion: { (true) in
//self.animationView.image = nil // Maybe we try the following?
//self.animationView.removeFromSuperview()
//self.animationView = nil
})
}
Based on SO responses, it looks like this may be the best method to prevent the images from being cached, but it doesn't seem to work in my code:
let image = UIImage(contentsOfFile: imageName)!
I have also tried this but it doesn't seem to work either:
func applicationDidReceiveMemoryWarning(application: UIApplication) {
NSURLCache.sharedURLCache().removeAllCachedResponses()
}
I also tried the following article (removeFromSuperview) in the completion block but I couldn't get this to work either (see my code above):
https://www.hackingwithswift.com/example-code/uikit/how-to-animate-views-using-animatewithduration
New Code:
// create the Animation Array for each animation
func createImageArray(total: Int, imagePrefix: String) -> [UIImage]{
var imageArray: [UIImage] = []
for imageCount in 0..<total {
let imageName = "\(imagePrefix)-\(imageCount).png"
//let image = UIImage(named: imageName)! // replaced with your code below
if let imagePath = Bundle.mainBundle.path(forResource: "ImageSet1" ofType: "png"),
let image = UIImage(contentsOfFile: imagePath) {
//Your image has been loaded }
imageArray.append(image)
}
return imageArray }
It's pretty simple. UIImage(named:) caches images, and UIImage(contentsOfFile:) does not.
If you don't want your images to be cached, use UIImage(contentsOfFile:) instead. If you can't get that to work then post your code and we'll help you debug it.
Be aware that UIImage(contentsOfFile:) does not look for files in your app bundle. It expects a full path to the image file. You will need to use Bundle methods to find the path to the file and then pass that path to UIImage(contentsOfFile:):
if let imagePath = Bundle.mainBundle.path(forResource: "ImageSet1" ofType: "png"),
let image = UIImage(contentsOfFile: imagePath) {
//Your image has been loaded
}
Your code is loading all the images for all 3 animations into an array of images and never releasing them, so the fact that the system caches those images seems pretty irrelevant. In a low memory condition the system should flush the cached copies of the images, but your code will still hold all those images in your arrays so the memory won't get freed. It looks to me like it's your code that's causing the memory problem, not the system's images caching.
Your code might look like this:
func createImageArray(total: Int, imagePrefix: String) -> [UIImage]{
var imageArray: [UIImage] = []
for imageCount in 0..<total {
let imageName = "\(imagePrefix)-\(imageCount)"
if let imagePath = Bundle.main.path(forResource: imageName,
ofType: "png"),
let image = UIImage(contentsOfFile: imagePath) {
imageArray.append(image)
}
}
return imageArray
}

Swift - Slow UITableView scroll (loading image from device storage)

I have a UITableView populated with cells either containing just a text label or a text label and a UIImage. When the app is run, images are downloaded from my server and stored on the device locally. Every time the app is run, the app checks for new images and downloads them. In my cellForRowAtIndexPath function, I load the image from the device storage on a background thread then update the UI on the main thread.
I don't know if I am doing this right but here is my code for displaying images inside my cellForRowAtIndexPath function:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
for image in self.postsImages {
if ((image as! NSArray)[0] as! Int == postIDCurrent) {
// there is an image associated with this post
let postImage = UIImage(contentsOfFile: (currentArray.lastObject as? String)!)
dispatch_async(dispatch_get_main_queue(), {
cell.postImageView.image = postImage
cell.postImageView.clipsToBounds = true
cell.postImageView.userInteractionEnabled = true
cell.postImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(PostsTableViewController.imageTapped(_:))))
})
}
}
})
The for and the if statements both just really check if there is an image associated with the current cell we are going to display.
I'm sorry if I'm not providing sufficient information. If you need anything more, please comment.
Anyways, my question is, what am I doing wrong with this code which causes my UITableView to scroll very slow?
Chances are that postImage is much larger than than the bounds of cell.postImageView, so the assignment on the main thread takes a noticeable amount of time to scale it down. Assuming that postImageView's bounds are foreseeable, scaling the image to the correct size in the background thread will be quite likely to sort this out. If you don't have a resizing function handy, this gist looks reasonable.
Try breaking your for loop:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
for image in self.postsImages {
if ((image as! NSArray)[0] as! Int == postIDCurrent) {
// there is an image associated with this post
let postImage = UIImage(contentsOfFile: (currentArray.lastObject as? String)!)
dispatch_async(dispatch_get_main_queue(), {
cell.postImageView.image = postImage
cell.postImageView.clipsToBounds = true
cell.postImageView.userInteractionEnabled = true
cell.postImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(PostsTableViewController.imageTapped(_:))))
})
break
}
}
})

SDWebImage implementation in parse using Swift

I am using parse to retrieve my images and labels and display it on a collection view. The problem was that the collection view loads all the images and labels at once making the load time long and memory usage was high. I was thinking that I would load 10 cells each time however I was recommended to use SDWebImage to make the app lighter. However I don't know how to implement it with parse using swift. I am suspecting that I would put some code in this piece of code below
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("newview", forIndexPath: indexPath) as! NewCollectionViewCell
let item = self.votes[indexPath.row]
let gesture = UITapGestureRecognizer(target: self, action: Selector("onDoubleTap:"))
gesture.numberOfTapsRequired = 2
cell.addGestureRecognizer(gesture)
// Display "initial" flag image
var initialThumbnail = UIImage(named: "question")
cell.postsImageView.image = initialThumbnail
// Display the country name
if let user = item["uploader"] as? PFUser{
item.fetchIfNeeded()
cell.userName!.text = user.username
var profileImgFile = user["profilePicture"] as! PFFile
cell.profileImageView.file = profileImgFile
cell.profileImageView.loadInBackground { image, error in
if error == nil {
cell.profileImageView.image = image
}
}
var sexInt = user["sex"] as! Int
var sex: NSString!
if sexInt == 0 {
sex = "M"
}else if sexInt == 1{
sex = "F"
}
var height = user["height"] as! Int
cell.heightSexLabel.text = "\(sex) \(height)cm"
}
if let votesValue = item["votes"] as? Int
{
cell.votesLabel?.text = "\(votesValue)"
}
// Fetch final flag image - if it exists
if let value = item["imageFile"] as? PFFile {
println("Value \(value)")
cell.postsImageView.file = value
cell.postsImageView.loadInBackground({ (image: UIImage?, error: NSError?) -> Void in
if error != nil {
cell.postsImageView.image = image
}
})
}
return cell
}
I have implemented SDWebImage using Pods and have imported through the Bridging Header. Is there anyone who knows how to implement SDWebImage with parse using Swift?
You should rethink your approach -
I believe you are using collectionViewDelegate method - collectionView(_:cellForItemAtIndexPath:)
this fires every time the collection view needs a view to handle.
In there you can access the cell imageView and set its image (For Example)-
cell.imageView.sd_setImageWithURL(url, placeholderImage:placeHolderImage, completed: { (image, error, cacheType, url) -> Void in })
And if you wish to fade in the image nicely, you could -
cell.imageView.sd_setImageWithURL(url, placeholderImage:placeHolderImage, completed: { (image, error, cacheType, url) -> Void in
if (cacheType == SDImageCacheType.None && image != nil) {
imageView.alpha = 0;
UIView.animateWithDuration(0.0, animations: { () -> Void in
imageView.alpha = 1
})
} else {
imageView.alpha = 1;
}
})
EDIT
I see the you use Parse, so you don't need SDWebImage, you need to use Parse - PFImageView, It will handle your background fetch for the image when it loads. You will need to save reference to your PFObject, but I believe you already do that.
For example (inside your cellForItemAtIndexPath)-
imageView.image = [UIImage imageNamed:#"..."]; // placeholder image
imageView.file = (PFFile *)someObject[#"picture"]; // remote image
[imageView loadInBackground];
How many objects are displaying in the collection view?
Since you mentioned SDWebImage, are you downloading the images in the background as well?
If you want to load the images as the user scrolls, have a look at the documentation for SDWebImage. The first use case describes how to display images in table view cells withouth blocking the main thread. The implementation for collection view cells should be similar.

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.

CKAsset won't show in tableview image

I have an optional image in the cloudkit DB(checked DB and the image is there in cases where I added it in my testing). I have created a class that initializes the record fields into variables I use in my tableview. I have a custom cell as well. But the image won't display in my custom tableview cell. I don't know if having an optional image in a tableview image is causing a problem or if there's an issue with my code/settings in cloudkit.
Any help is greatly appreciated, as I've been stuck for over a week and there's little to nothing online about this.
Here's my class code:
var feedResults = [UserPosts]()
class UserPosts {
var record: CKRecord
var description: String
var username: String
//var image: CKAsset? // tried also initializing the CKAsset separately
var postPic: UIImage?
init(record: CKRecord) {
self.record = record
self.description = record.objectForKey("description") as String
self.username = record.objectForKey("username") as String
// self.image = record.objectForKey("postPic") as? CKAsset
if var pic = record.objectForKey("postPic") as CKAsset! {
self.postPic = UIImage(contentsOfFile: pic.fileURL.path!)
// below is the other way I've seen it done.
// var url = pic.fileURL!
// var imageData = NSData(contentsOfFile: url.path!)
// self.postPic = UIImage(data: imageData!)
}
}
}
Here's my tableview code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as TableViewCell
let record = feedResults[indexPath.row]
cell.postDescription.text = record.description
cell.userName.text = record.username
if record.postPic != nil {
cell.postPic.image = record.postPic!
}
return cell
}
Also I've seen a couple ways of pulling a CKAsset into a UIImage. The first way, is how I saw Apple do it in their CloudKitAtlas project. Although I'm not well versed in Obj-C, I think I followed it correctly - CloudKitAtlas Example
I've also seen it done using NSData(contentOFFile:) and UIImage(data:), as shown in this post here: SO CKAsset Example
Try doing it without the .paths and using contentsOfURL. This is how I do this (within your if for the CKAsset):
if var data = NSData(contentsOfURL: pic!.fileURL) {
self.postPic = UIImage(data: data!)!
}
And besides that... You do a dequeueReusableCellWithIdentifier but don't check if it returns nil. You should create a new cell instance If it was nil
You can display the image from the CKAsset like this:
var img = record.valueForKey("Picture") as? CKAsset
cell.imageView?.image = UIImage(contentsOfFile: img!.fileURL.path!)
Hope this helps!

Resources