Update table cell image asynchronously - ios

Im downloading the image link from a json and then creating the image once the table view start creating its cells:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCellController
DispatchQueue.main.async(execute: { () -> Void in
if let url = NSURL(string: self.movies[indexPath.row].image)
{
if let data = NSData(contentsOf: url as URL)
{
let imageAux = UIImage((data: data as Data))
cell.movieImage.image = imageAux
self.tableView.reloadData()
}
}
})
cell.name = self.movies[indexPath.row].name
cell.date = self.movies[indexPath.row].date
return cell
}
And this works fine, but the table view becomes really slow, not at rendering but at scrolling. I keep checking the RAM and CPU and both are really low but my network use keeps rising BUT the images are already on the cell so it means its already done. (For this test im calling the JSON for only 2 movies, so 2 images)
Before i started doing this my total download was about 200kb (with images), now its getting over 2MB before i stop the project.
What im doing wrong?

You'll probably want to designate a separate queue for background activities. In this instance, your heavy network task is in:
NSData(contentsOf: url as URL)
This is what is "freezing" the UI. The best solution would be to define something like DispatchQueue.background and perform the network calls there, while then later performing the UI tasks back on the main thread so as not to lock up your display:
DispatchQueue.background.async(execute: { () -> Void in
if let url = NSURL(string: self.movies[indexPath.row].image) {
//Do this network stuff on the background thread
if let data = NSData(contentsOf: url as URL) {
let imageAux = UIImage(data: data as Data)
//Switch back to the main thread to do the UI stuff
DispatchQueue.main.async(execute: { () -> Void in
cell.movieImage.image = imageAux
})
}
}
})
Let me know if this makes sense.

Related

Error loading image from Firebase to my Table View

I want to load my images from Firebase to my Table View but I get the error:
Cannot convert value of type 'String' to expected argument type 'URL'
When I print the object on its own it is definitely a URL.
This is what my code looks like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
//TODO: Guard...
let postImage = postArray [indexPath.row]
let postImageURL = postImage.postImageURL
let data = Data(contentsOf: postImageURL) // Line with Error
cell.postImage.image = UIImage (data: data)
return cell
}
To display the image in your cell, you need to convert the URL string into an actual URL object, which you can do via:
let postImage = postArray[indexPath.row]
if let postImageURL = URL(string: postImage.postImageURL)
{
do {
let data = try Data(contentsOf: postImageURL)
cell.postImage.image = UIImage (data: data)
} catch {
print("error with fetching from \(postImageURL.absoluteString) - \(error)")
}
}
And as rmaddy implies, your performance is not going to be very good (because depending on how far away the remote server is or how slow the internet is), the synchronous "Data(contentsOf:" call might take an unacceptably long time to succeed. I'm just providing this answer so you will be able to see something in your own testing, but I wouldn't use this in production code.
Try to replace the Data fetch with an asynchronous URLSession task, and you can find much more information in this very related question.

Download and cache images in UITableViewCell

Note: Please no libraries. This is important for me to learn. Also, there are a variety of answers on this but none that I found solves the issue nicely. Please don't mark as duplicate. Thanks in advance!
The problem I have is that if you scroll really fast in the table, you will see old images and flickering.
The solution from the questions I read is to cancel the URLSession
data request. But I do not know how to do that at the correct place
and time. There might be other solutions but not sure.
This is what I have so far:
Image cache class
class Cache {
static let shared = Cache()
private let cache = NSCache<NSString, UIImage>()
var task = URLSessionDataTask()
var session = URLSession.shared
func imageFor(url: URL, completionHandler: #escaping (image: Image? error: Error?) -> Void) {
if let imageInCache = self.cache.object(forKey: url.absoluteString as NSString) {
completionHandler(image: imageInCache, error: nil)
return
}
self.task = self.session.dataTask(with: url) { data, response, error in
if let error = error {
completionHandler(image: nil, error: Error)
return
}
let image = UIImage(data: data!)
self.cache.setObject(image, forKey: url.absoluteString as NSString)
completionHandler(image: image, error: nil)
}
self.task.resume()
}
}
Usage
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let myImage = images[indexPath.row]
if let imageURL = URL(string: myImage.urlString) {
photoImageView.setImage(from: imageURL)
}
return cell
}
Any thoughts?
Swift 3:
Flickering can be avoided by this way:
Use the following code in public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
cell.photoImageView.image = nil //or keep any placeholder here
cell.tag = indexPath.row
let task = URLSession.shared.dataTask(with: imageURL!) { data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async() {
if cell.tag == indexPath.row{
cell.photoImageView.image = UIImage(data: data)
}
}
}
task.resume()
By checking cell.tag == indexPath.row, we are assuring that the imageview whose image we are changing, is the same row for which the image is meant to be. Hope it helps!
A couple of issues:
One possible source of flickering is that while you're updating the image asynchronously, you really want to clear the image view first, so you don't see images for prior row of reused/dequeued table view cell. Make sure to set the image view's image to nil before initiating the asynchronous image retrieval. Or, perhaps combine that with "placeholder" logic that you'll see in lots of UIImageView sync image retrieval categories.
For example:
extension UIImageView {
func setImage(from url: URL, placeholder: UIImage? = nil) {
image = placeholder // use placeholder (or if `nil`, remove any old image, before initiating asynchronous retrieval
ImageCache.shared.image(for: url) { [weak self] result in
switch result {
case .success(let image):
self?.image = image
case .failure:
break
}
}
}
}
The other issue is that if you scroll very quickly, the reused image view may have an old image retrieval request still in progress. You really should, when you call your UIImageView category's async retrieval method, you should cancel and prior request associated with that cell.
The trick here is that if you're doing this in a UIImageView extension, you can't just create new stored property to keep track of the old request. So you'd often use "associated values" to keep track of prior requests.

Swift - Concurrency issue when loading search result. I load the images, send the requests, but the requests are too late

So I have an app that loads movie search results from the web live as the user types. I throttle my search requests so that it only reloads once every 0.3 seconds. (This probably isn't relevant at all, but what the hell). Now my problem is this.
1 - I type in a search term, let's say "Search1". In order to save time, I load up each result's title, year and genre instantly (almost). I keep the poster black, and send an asynchronous request to load the image, because it takes a lot more time. I wait for the image to load.
2 - Before the images load, I then type in another term, let's say "Search2". So I get the text results for "Search2", and maybe some images.
3 - But then the old requests for "Search1" start rolling in, and they replace those of Search2, because they loaded slower. What I get is a combination of old and new images because I couldn't cancel the old requests.
How should I solve this? I need a way to tell the device to stop loading the old images if the user started typing again, and I can't cancel asynchronous requests. How do I fix this?
Code:
The cell throttling stuff
func updateSearchResultsForSearchController(searchController: UISearchController) {
// to limit network activity, reload 0.3 of a second after last key press.
NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "reload:", object: searchController)
if (!UIApplication.sharedApplication().networkActivityIndicatorVisible) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
self.tableView.reloadData()
}
self.performSelector("reload:", withObject: searchController, afterDelay: 0.3)
}
Here is the code that loads up each cell's info:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell")! as! CustomCell
//Safe-Guard. This shouldn't be needed if I understood what I was doing
if (indexPath.row < matchingItems.count) {
cell.entry = matchingItems[indexPath.row] //404
/*
.
.
omitted code that loads up text info (title, year, etc.)
.
.
*/
//Poster
cell.poster.image = nil
if let imagePath = matchingItems[indexPath.row]["poster_path"] as? String {
//Sessions and Stuff for request
let url = NSURL(string: "http://image.tmdb.org/t/p/w185" + imagePath)
let urlRequest = NSURLRequest(URL: url!)
let session = NSURLSession.sharedSession()
//Asynchronous Code:
let dataTask = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, response, error) -> Void in
if let poster = UIImage(data: data!) {
//I want to stop this piece from running if the user starts typing again:
dispatch_async(dispatch_get_main_queue()) {
//Animate the poster in
cell.poster.alpha = 0.0 // hide it
cell.poster.image = poster // set it
UIView.animateWithDuration(1.0) {
cell.poster.alpha = 1.0 // fade it in
}
}
}
})
dataTask.resume()
} else {
//Just use placeholder if no image exists
cell.poster.image = UIImage(named: "Placeholder.jpg")
}
}
return cell
}
You can just check the indexPath of the cell and see if it's the same or if it's been dequeued and reused for another another row.
let dataTask = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, response, error) -> Void in
if let poster = UIImage(data: data!) {
//I want to stop this piece from running if the user starts typing again:
dispatch_async(dispatch_get_main_queue()) {
//Animate the poster in
if tableView.indexPathForCell(cell) == indexPath {
cell.poster.alpha = 0.0 // hide it
cell.poster.image = poster // set it
UIView.animateWithDuration(1.0) {
cell.poster.alpha = 1.0 // fade it in
}
}
}
}
})
For future people stuck with the same issue, I used Alamofire, a networking library that allows you to cancel asynchronous requests. If you use Objective-C then look up AFNetworking. It's the same thing but for Objective-C.

Images not displayed properly when scrolled iOS Swift

In my app i am having tableview with sections.The issue is with images. When the user scrolls the list the images displayed are not proper.I know the issue is because of the recycling but still i cannot find any solution.
Code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myeventCell = self.tableView.dequeueReusableCellWithIdentifier("MyEventsTableViewCell", forIndexPath: indexPath) as! MyEventsTableViewCell
myeventCell.wedImage.clipsToBounds=true;
myeventCell.tag=indexPath.row+indexPath.section;
//to download image
if wedImgDownload[indexPath.section][indexPath.row] == false
{
// myeventCell.wedImage.image = UIImage(data: self.webImgData[indexPath.section][indexPath.row]);
if let url = NSURL(string: wedImageUrl[indexPath.section][indexPath.row] as String) {
let request = NSURLRequest(URL: url)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {
(response: NSURLResponse?, data: NSData?, error: NSError?) -> Void in
if let imageData = data as NSData? {
if myeventCell.tag == indexPath.row+indexPath.section {
self.wedImgDownload[indexPath.section].removeAtIndex(indexPath.row)
self.wedImgDownload[indexPath.section].insert(true, atIndex: indexPath.row)
self.webImgData[indexPath.section].insert(data!, atIndex: indexPath.row)
myeventCell.wedImage.image = UIImage(data: imageData);
}
}
}
}
}
else{
if self.webImgData[indexPath.section][indexPath.row] != ""{
if myeventCell.tag == indexPath.row+indexPath.section {
myeventCell.wedImage.image = UIImage(data: self.webImgData[indexPath.section][indexPath.row])
}
}
}
return myeventCell;
}
Please lemme know how to solve this issue?
It's because the cells are being reused. The cells are kicking off requests each time they are reused. The order in which the requests finish can't be determined since they are asynchronous. Once a request does finish and the image is set, the cell gets reused and shows the previous image while the current request is in progress.
NSURLConnection is deprecated, you should be using NSURLSession. You will need to cache these images instead of kicking off a request each time they are displayed. You will also need to clear the image each time a cell is reused so it doesn't show the previous image when displayed.
There are many open source libraries available which do exactly these things and are extremely well tested and used by millions of users. It would be foolish to not take advantage of them unless it is a hard requirement of the project.
https://github.com/pinterest/PINRemoteImage
https://github.com/rs/SDWebImage
https://github.com/Alamofire/AlamofireImage

Image Loading takes too long using https or encoding/decoding db blobs

I've tried loading images from my database(encoding decoding medium blobs) and I've also tried storing the images on my server but it takes way too much time to load when I'm searching for 10+ users and attaching images to the cell. The search works extremely fast without images...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = self.myTable.dequeueReusableCellWithIdentifier("Cell")
if (self.countrySearchController.active)
{
cell!.textLabel?.text! = self.searchArray[indexPath.row]
if (cell!.textLabel!.text! != "")
{
let imageData = NSData(contentsOfURL: NSURL(string: "https://www.mywebsite.com/profileimages/\(cell!.textLabel!.text!).jpg")!)
if imageData != nil
{
let d = UIImage(data: imageData!)
cell!.imageView?.image = d
}
}
return cell!
}
else
{
cell!.textLabel!.text! = MyVariables.users[indexPath.row] as! String
return cell!
}
}
}
You are downloading image synchronously, thats why its taking long to perform, i have added Async GCD block to download image and set. Replace your cellForRowAtIndexPath method with following code.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = self.myTable.dequeueReusableCellWithIdentifier("Cell")
if (self.countrySearchController.active)
{
cell!.textLabel?.text! = self.searchArray[indexPath.row]
if (cell!.textLabel!.text! != "")
{
// *** GCD queue to perform Asynchronous Task ***
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Perform Image Downloading Task here
let imageData = NSData(contentsOfURL: NSURL(string: "https://www.mywebsite.com/profileimages/\(cell!.textLabel!.text!).jpg")!)
dispatch_async(dispatch_get_main_queue(), ^{
// update your Imageview with Downloaded Image
if imageData != nil
{
let d = UIImage(data: imageData!)
cell!.imageView?.image = d
}
});
});
}
return cell!
}
else
{
cell!.textLabel!.text! = MyVariables.users[indexPath.row] as! String
return cell!
}
}
Even to enhance performance you can save downloaded image into your app's Documents directory and load image from it next onwards.
In your case you can save Images into NSCache and display images from it once its downloaded.
You should load your images using multitasking. Easiest way to do this is using SDWebImage framework. It's also allows caching and setting placeholders. All what you need with this framework is something like this:
cell.photoView.sd_setImageWithURL(NSURL(string: friend.imageUrl), placeholderImage: placeHolderImage)

Resources