Slow loading of images in UITableViewController extracted from URL string - ios

I'm trying to load images extracted from the web URL into the image view of each cell.
However, when i scroll the table the screen will freeze as I believe it is attempting to grab the images for each cell 1 by 1.
Is there a way i can make it asynchronous? The resources available out there currently is outdated or incompatible(running obj c) as I'm running on Swift 2
The relevant code I'm using within the table view controller is below :
override func tableView(newsFeedTableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let blogPost: BlogPost = blogPosts[indexPath.row]
cell.textLabel?.text = blogPost.postTitle
let unformattedDate = blogPost.postDate
//FORMATTING: Splitting of raw data into arrays based on delimiter '+" to print only useful information
let postDateArr = unformattedDate.characters.split{$0 == "+"}.map(String.init)
cell.detailTextLabel?.text = postDateArr[0]
let url = NSURL(string: blogPost.postImageUrl)
let data = NSData(contentsOfURL: url!)
cell.imageView!.image = UIImage(data: data!)//WHY SO SLOW!?
print(blogPost.postImageUrl)
return cell
}

Try this
var image: UIImage
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {() -> Void in
// Background thread stuff.
let url = NSURL(string: blogPost.postImageUrl)
let data = NSData(contentsOfURL: url!)
image = UIImage(data:data)
dispatch_async(dispatch_get_main_queue(), {() -> Void in
// Main thread stuff.
cell.imageView.image = image
})
})

Lets clean your code a bit. First of all, you are trying to declear ALL your cells in your viewController. That means your app is not trying to load every image one byt one, but more like everything all together.
You need to create a separate file called PostCell what is going to be a type of UITableViewCell.
Then you need to go to your prototype cell and connect those view elements to that PostCell just like you would add those to any other ViewController.
Now, here is new code to your cellForRowAtIndexPath function:
override func tableView(newsFeedTableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let blogPost = blogPosts[indexPath.row]
if let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? PostCell {
cell.configureCell(blogPost)
return cell
}
return UITableViewCell() // You need to do this because of if let
}
And declear this on that PostCell:
func configureCell(post: BlogPost) {
this.textLabel.text = post.postTitle
let postDateArr = unformattedDate.characters.split{$0 == "+"}.map(String.init)
this.detailTextLabel.text = postDateArr[0]
// I would add few if let declarations here too, but if you are sure all these forced ! variables do exciest, then its ok
let url = NSURL(string: blogPost.postImageUrl)
let data = NSData(contentsOfURL: url!)
this.imageView.image = UIImage(data: data!)
}
Or something along those lines. When you connect those elements to your cell, you will get proper variable names for those.
That SHOULD help. There are plenty of tutorials how to make a custom tableviewcell. Some of them advice to put all the declarations inside that cellForRowAtIndexPath, but I have found that it get's problematic very fast.
So...my advice in a nutscell...create a custom tableviewcell.
Hope this helps! :)

To load the image on every cell use SDWebImage third party library. You can add it using pods as put pod 'SDWebImage' It provides various methods to load the image with caching or without caching asynchronously. With caching you don't really need to worry about loading image data every time cell appears on the screen. Try this
override func tableView(newsFeedTableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? PostCell {
--reset your cell here--
// cell.imageView.image = nil
}
cell.imageView.sd_setImageWithURL(YOUR_URL, placeholderImage: UIImage(named: "")) {
(UIImage img, NSError err, SDImageCacheType cacheType, NSURL imgUrl) -> Void in
// Do awesome things
}
-- configure your cell here --
}

Related

Swift tableView with images from API is lagging when scrolling

I have a tableView which I populate with images from an API,
however, it is lagging quite a bit when scrolling. I've tried to use async & run on different threads but cannot get it right.. so how do I fix this?
This is the cellForRowAt-func that I use to set image for every single cell.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "VideoCell", for: indexPath) as! TableViewCell
let video = highlightsArray[indexPath.row]
cell.video = video
do {
let url = URL(string: self.highlightsArray[indexPath.row].thumbnail)!
let data = try Data(contentsOf: url)
cell.thumbnailImage.image = UIImage(data: data)
}
catch{
print(error)
cell.thumbnailImage.image = UIImage(named: "pllogo.jpg")
}
return cell
}
This line
let data = try Data(contentsOf: url)
blocks the main thread and redownloads the same image multiple times when scrolling , consider using SDWebImage

UITableViewCell image loading wrong images

For now i download the image, story it in a mutable dictionary and then verify if the image was already downloaded and if not, download it and store it. As a key i use the indexPath.
This code kinda works, but from the tests i did if i scroll too fast the cell image will load the wrong one and after a split of a second it will load the right one (replacing the wrong image).
Im always clearing my thumbnail (imageView) after i call the method so i don't know why im getting this bug.
I though that maybe the if(self.imageCache.object(forKey: cacheKey) != nil) statement was true and thats why i would get multiple images, but the breakpoint didn't stop at once when i was scrolling down.
Any ideas?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MovieCellController
cell.thumbnail.image = UIImage()
let cacheKey = indexPath.row
if(self.imageCache.object(forKey: cacheKey) != nil)
{
cell.thumbnail.image = self.imageCache.object(forKey: cacheKey) as? UIImage
}
else
{
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
if let url = NSURL(string: self.moviesCollection[indexPath.row].imageLink) {
if let data = NSData(contentsOf: url as URL) {
let image: UIImage = UIImage(data: data as Data)!
self.imageCache.setObject(image, forKey: cacheKey as NSCopying)
DispatchQueue.main.async(execute: {
cell.thumbnail.image = image
})
}
}
}
}
cell.name.text = moviesCollection[indexPath.row].name
return cell
}
It is happening because the cells are reused due to which when scrolling fast the image of another cell seems to be assigned, but if fact it is the previous cell's image which is reused.
In cell's prepareForReuse method set your imageView's image to nil. Like, imageView.image = nil
Because the cell is reused.
The reused-cell keeps its old data.
The new image downloading will cost few seconds so that the reused -cell cannot change the image immediately.
You can use a placeholder-image when downloading the new image.
Or you can use the 3rd part library - SDWebImage.

IOS Swift how can hide the space of an Image inside a TableView

I have a tableView that contains a UIImageView and if the Image has a URL then the image displays and if not then no Image is displayed. The issue I have is that if there is no Image then a big blank spot occurs in the TableView as if there was an image. Is there a way to reduce the blank spot or hide it (Images below) ? The image is the big UIImageView in the center . This is my code when it comes to the Images
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyFeed", for: indexPath) as! MyFeed
if stream_image_string[indexPath.row].characters.count > 2 {
let strCellImageURL = self.stream_image_string[indexPath.row]
let imgURL: NSURL = NSURL(string: strCellImageURL)!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
cell.post_image.image = UIImage(data: data!)
})
});
task.resume()
} else {
cell.post_image!.isHidden = true
cell.post_image!.image = nil
}
return cell
}
Essentially if the String coming back has 2 or more characters then it's a valid URL and the image is downloaded; the part that I am focused on is the else statement and this code
else {
cell.post_image!.isHidden = true
cell.post_image!.image = nil
}
So obviously if it goes in the else statement then there is no image and I set the Image to null or nil then I try to hide the extra white space by setting the Image to hidden however that does not work . Any idea on how I can hide the white space ? I have also been reading this question but it does not work iOS swift imageView cannot be hidden in TableViewCell
Give outlet of image's width and if there is no image then set constant of that outlet to "0".
e.g.
if(!image)
{
widthOfImage.constant = 0
}
I experienced the same problem myself. You need to create 2 cells for this. Like this:
override func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (stream_image_string[indexPath.row] == "")
{
let cell = tableView.dequeueReusableCell (withIdentifier: "noImageMyFeed", for: indexPath) as! noImageMyFeed
return cell
}
else
{
let cell = tableView.dequeueReusableCell (withIdentifier: "MyFeed", for: indexPath) as! MyFeed
return cell
}
}
this video will help you in detail : https://www.youtube.com/watch?v=FAxtWtqeMIM
adapt the video to its own content, create a cell from scratch by simply deleting the image part

Images not show in sequence in UITableViewCell

I'm trying to show images from XML enclosure to tableViewCell image. Images are show but not in sequence, due to dequeueReusableCellWithIdentifier because when i scroll tableViewCell up and down it change images and not show in sequence according to array index. I've tried different ways but did't get success'
Can anyone please tell me how can show images in sequence, or is there any way that first download all images and then show in cell image??
Or any other quick or easy method instead using dispatch_async.
Thanks
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
downloadFileFromURL(NSURL(string: self.posts.objectAtIndex(indexPath.row).valueForKey("enclosure") as! String)!, completionHandler:{(img) in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.sideImageView.image = img
})
})
return cell
}
UPDATE
Now i tried this
let picURL = self.posts.objectAtIndex(indexPath.row).valueForKey("enclosure") as! String
let url = NSURL(string: picURL)
let data = NSData(contentsOfURL: url!)
cell.sideImageView?.image = UIImage(data: data!)
It show images in sequence but make scrolling hard?
Update2
Now i've tried this
var check = true
var imageArrayNsData : [NSData] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
if check == true{
var indeX = 0
for i in posts.valueForKey("enclosure") as! [NSString]{
let picURL = self.posts.objectAtIndex(indeX).valueForKey("enclosure") as! String
let url = NSURL(string: picURL)
let data = NSData(contentsOfURL: url!)
print("download")
imageArrayNsData.append(data!)
indeX++
print(indeX)
}
check = false
}
if check == false{
cell.sideImageView.image = UIImage(data: imageArrayNsData[indexPath.row])
}
return cell
}
This method only download images one time. And after downloading images it appends in array and next time it show images from array without downloading again. But this method is little bit hard for scrolling. Any one have idea why?
The problem is that the cell object may have been already reused by the time you set the image. You need to add a check to make sure the cell still represents the content you want. That could be as simple as:
if tableView.indexPathForCell(cell) == indexPath {
cell.sideImageView.image = img
}
But might need to be more complex if the index path for a specific item might change in that time (for example, if the user can insert/delete rows).
You could also use a library like AlamofireImage which handles this work (in a different way) for you. With AlamofireImage, your code would look like:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
let URL = NSURL(string: self.posts.objectAtIndex(indexPath.row).valueForKey("enclosure") as! String)!
cell.sideImageView.af_setImageWithURL(URL)
return cell
}
To download asynchronously images and set to UIImageView of your UITableViewCell, you can add an extension to your UIImageView.
extension UIImageView {
func downloadImageFrom(link link:String, contentMode: UIViewContentMode) {
//in my methods, I have a cache to avoid re-downloading my images. Images in cache are identified by its URL
if let _imageData = ImageCache.shareCache.getImageData(link) {
self.image = UIImage(data: _imageData)
return
}
//else, download image
NSURLSession.sharedSession().dataTaskWithURL( NSURL(string:link)!, completionHandler: {
(data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue()) {
self.contentMode = contentMode
if let data = data {
ImageCache.shareCache.cacheImageData(data, imageId: link)
self.image = UIImage(data: data)
}
}
}).resume()
}
}
then, from your call-back cellforrow,
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
cell.imageView.downloadImageFrom(yourImageUrl)
return cell

Autolayout, async load image into uiimageview (Swift, xCode6)

Good day! I have 2 problems and I hope you will help me.
1) I have news feed in my application, that contains images.
I am using Autolayout for dynamic cells:
and I want the image to keep its ratio and to completely fill the width of the cell (with margins = 12).
I set constrains, cell is autoresizable, but image didn't save its ratio:
.
What I am doing wrong?
2) The second problem, i load images asynchronously, here is my code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: EventCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as EventCell
var rowData: NSDictionary = tableData[indexPath.row] as NSDictionary
cell.titleButton.setTitle(rowData["title"] as? String, forState: UIControlState.Normal)
cell.titleButton.addTarget(self, action: "openWebSite:", forControlEvents: UIControlEvents.TouchUpInside)
cell.titleButton.tag = indexPath.row
cell.descriprionLabel.text = rowData["description"] as? String
var urlString = rowData["image"] as String
var image = imageCache[urlString]
if( image == nil ) {
var imgURL = NSURL(string: urlString)
// Download an NSData representation of the image at the URL
var request: NSURLRequest = NSURLRequest(URL: imgURL!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
if error == nil {
image = UIImage(data: data) // data -> image
// Store the image in to our cache
self.imageCache[urlString] = image // save in our dictionary
if let cellToUpdate : EventCell = tableView.cellForRowAtIndexPath(indexPath) as? EventCell {
self.table.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
else {
println("Error: \(error.localizedDescription)")
}
})
} else {
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate : EventCell = tableView.cellForRowAtIndexPath(indexPath) as? EventCell {
cellToUpdate.img.image = image
}
})
}
cell.selectionStyle = UITableViewCellSelectionStyle.None;
cell.contentView.setNeedsLayout(); // autolayout bug solution
cell.contentView.layoutIfNeeded(); // autolayout bug solution
return cell
}
All seems okay, but UITableViewCell don't resize when image is loaded and I am trying to reload cell at index path.
Interesting moment, that it will work if I scroll down and then come back to cell.
I have similar error before and I fixed it reading this article UITableView layout messing up on push segue and return. (iOS 8, Xcode beta 5, Swift) , third answer. But it didn't help me now. Looks like I need to call some method to recalculate UITableViewCell, but I don't understand what.
First question : Change UIImageView view mode from Scale to Fill to Aspect Fit (in storyboad)
Second question : Remove dispatch async if image is not nil and make you code look similar like this:
if( image == nil ) {
...
}
else {
cell.img.image = image
}
For first one in the storyboard select the image view and click on the pin icon for setting auto layout constraint and check width and height.
it will remain the width and height constant.

Resources