UITableViewCell shows the wrong image while images load - swift

I have UITableView that lists social media posts with images in them.
Once all the post details have loaded and the images cached it looks great but while it loads it often shows the wrong image with the wrong post.
I have been struggling and coming back to this issue for months. I don't think it is a loading issue it almost looks like iOS dumps the image an any old cell until it finds the right one but honestly I'm out of ideas.
Here is my image extension that also takes care of the caching:
let imageCache = NSCache<NSString, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = UIImage(named: "loading")
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//No cache, so create new one and set image
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
}
And this is a shortened version of my UITableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let postImageIndex = postArray [indexPath.row]
let postImageURL = postImageIndex.postImageURL
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
cell.delegate = self
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
cell.postTitle.text = postArray [indexPath.row].postTitle
cell.postDescription.text = postArray [indexPath.row].postBody
return cell
}
FeedItem Class includes prepareForReuse() and looks like this:
override func prepareForReuse() {
super.prepareForReuse()
self.delegate = nil
self.postHeroImage.image = UIImage(named: "loading")
}
EDIT: Here is my method for retrieving data from Firebase:
func retrievePosts () {
let postDB = Database.database().reference().child("MyPosts")
postDB.observe(.childAdded) { (snapshot) in
let snapshotValue = snapshot.value as! Dictionary <String,AnyObject>
let postID = snapshotValue ["ID"]!
let postTitle = snapshotValue ["Title"]!
let postBody = snapshotValue ["Description"]!
let postImageURL = snapshotValue ["TitleImage"]!
let post = Post()
post.postTitle = postTitle as! String
post.postBody = postBody as! String
post.postImageURL = postImageURL as! String
self.configureTableView()
}
}

UITableView only uses a handful of cells (~ the max number of visible cells on screen) when displaying a collection of items, so you'll have more items than cells. This works because of the table view reusing mechanism, which means that the same UITableViewCell instance will be used for displaying different items. The reason why you are having problems with the images is because you aren't handling the cell reusing properly.
In the cellForRowAt function you call:
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
While you scroll the table view, in different invocations of cellForRowAt this function will be called for the same cell, but (most probably) displaying the content of different items (because of the cell reusing).
Let's X be the cell you are reusing, then these are roughly the functions that will be called:
1. X.prepareForReuse()
// inside cellForRowAt
2. X.postHeroImage.loadImageUsingCacheWithUrlString(imageA)
// at this point the cell is configured for displaying the content for imageA
// and later you reuse it for displaying the content of imageB
3. X.prepareForReuse()
// inside cellForRowAt
4. X.postHeroImage.loadImageUsingCacheWithUrlString(imageB)
When the images are cached, then you will always have 1, 2, 3 and 4 in that order, that's why you don't see any issues in that case. However, the code that downloads an image and set it to the image view runs in a separate thread, so that order isn't guaranteed anymore. Instead of only the four steps above, you will have something like:
1. X.prepareForReuse()
// inside cellForRowAt
2. X.postHeroImage.loadImageUsingCacheWithUrlString(imageA)
// after download finishes
2.1 X.imageView.image = downloadedImage
// at this point the cell is configured for displaying the content for imageA
// and later you reuse it for displaying the content of imageB
3. X.prepareForReuse()
// inside cellForRowAt
4. X.postHeroImage.loadImageUsingCacheWithUrlString(imageB)
4.1 X.imageView.image = downloadedImage
In this case, because of concurrency, you could end up with the following cases:
1, 2, 2.1, 3, 4, 4.1: Everything is displayed properly (this will happen if you scroll slowly)
1, 2, 3, 2.1, 4, 4.1: In this case the first image finishes downloading after the call to reuse the cell finishes, so the old image will be displayed (wrongly) for a short period of time while the new one is downloaded, and then replaced.
1, 2, 3, 4, 2.1, 4.1: Similar to the case above.
1, 2, 3, 4, 4.1, 2.1: In this case the old image finishes downloading after the new one (there is no guaranty the downloads finish in the same order they started) so you will end up with the wrong image. This is the worst case.
For fixing this problem, let's turn our attention to the problematic piece of code inside the loadImageUsingCacheWithUrlString function:
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
// this is the line corresponding to 2.1 and 4.1 above
self.image = downloadedImage
}
})
}).resume()
As you can see, you are setting self.image = downloadedImage even when you aren't displayed the content associated to that image anymore, so what you need is some way to check if that's still the case. Since you define loadImageUsingCacheWithUrlString in an extension for UIImageView, then you don't have much context there to know whether you should display the image or not. Instead of that, I propose to move that function to an extension of UIImage that will return that image in a completion handler, and then call that function from inside your cell. It would look like:
extension UIImage {
static func loadImageUsingCacheWithUrlString(_ urlString: String, completion: #escaping (UIImage) -> Void) {
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
completion(cachedImage)
}
//No cache, so create new one and set image
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
completion(downloadedImage)
}
})
}).resume()
}
}
class FeedItem: UITableViewCell {
// some other definitions here...
var postImageURL: String? {
didSet {
if let url = postImageURL {
self.image = UIImage(named: "loading")
UIImage.loadImageUsingCacheWithUrlString(url) { image in
// set the image only when we are still displaying the content for the image we finished downloading
if url == postImageURL {
self.imageView.image = image
}
}
}
else {
self.imageView.image = nil
}
}
}
}
// inside cellForRowAt
cell.postImageURL = postImageURL

Another way to deal with this problem will be by using tableView(_:willDisplay:forRowAt:) for loading downloaded images from the cache and tableView(_:didEndDisplaying:forRowAt:) for removing the image from the cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
cell.delegate = self
cell.postTitle.text = postArray [indexPath.row].postTitle
cell.postDescription.text = postArray [indexPath.row].postBody
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let feedCell = cell as! FeedItem
if downloadImages.count > 0 {
cell.postHeroImage.image = downloadImages[indexPath.row]
}
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let feedCell = cell as! FeedItem
cell.postHeroImage.image = nil
}
If you are using CocoaPods, I would strongly recommend using Kingfisher for dealing with image downloads for your project

You need to download and cached the image based on the image URL itself and use it when load the table with that url.
Let's say you have an array of image URLs load the number of rows with this array and download image should be mapped to indexPath and cached. Later you can use it based on the indexPath with array.
The issue which you are facing is not sync row with mapped data in downloaded image. As TableViewCell deque and reuse the cell.

That is because tableView reuses it cells. So one cell could be responsible for multiple images with different urls.
So there is a simple solution for this:
Instead of passing the reference to the reusable cell, you should pass the IndexPath. It's value type and would not reuse.
Then when you have got your image from the async task, you can ask the TableView for the actual cell with .cellForRow(at: indexPath) function.
So, get rid of this line:
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
and replace it with a function that takes the actual indexPath and maybe a reference to the tableView.
Watch this WWDC 2018 session for more information. It's about UICollectionView but same as UITableView.
Also you can get the indexPath and the tableView from the cell itself like this answer but make sure you done it BEFORE calling the async function.

you are using in your cellForRowAt function with a reusable cells, although the cell is ever load and unload information, we both know that when a picture is downloading, the downloading is not quick, you need download your images in any function except cellForRowAt. for example
if you have an array of urls
let arrayImages = ["url1", "url2", "url3"]
let downloadImages = [UIImage]()
var dispatchGroup = DispatchGroup()
extension for UIImage
import Foundation
import UIKit
extension UIImage {
func downloaded(from url: URL, completion: ((UIImage,String) -> Void)?) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
DispatchQueue.global().async() {
completion?(image,url.absoluteString)
}
}.resume()
}
func downloaded(from link: String, completion: ((UIImage,String) -> Void)?) {
guard let url = URL(string: link) else { return }
downloaded(from: url, completion: completion)
}
}
code for your view
override func viewWillAppear()
{
super.viewWillAppear(true)
for url in arrayImages
{
dispatchGroup.enter()
let imageDownloaded = UIImage()
imageDownloaded.downloaded(from: url) { (image, urlImage2) in
DispatchQueue.main.async {
self.downloadImages.append(image)
self.tableView.reloadData()
self.dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
cell.delegate = self
if downloadImages.count > 0 {
cell.postHeroImage.image = downloadImages[indexPath.row]
}
cell.postTitle.text = postArray [indexPath.row].postTitle
cell.postDescription.text = postArray [indexPath.row].postBody
return cell
}
if you have any doubts, please tell me. I will hope that this can help you

Related

Loading images async in tableview

I've written a async method to load pictures into a tableview. But the problem is that the image is not loading because it will not update automatically only after scrolling. I have tried to add:
tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.none)
But this will only keep refreshing the row the whole time with different images. The second problem is that after scrolling the images will show but sometimes the images will load in the wrong row.
Could please someone help me with this problem because i can't figure it out.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ArticleCell", for: indexPath)
let article = newsArticles!.articles[indexPath.row]
cell.textLabel?.text = article.title
cell.detailTextLabel?.text = article.content
cell.imageView?.image = nil
if let url = article.urlToImage {
imageLoader.obtainImageWithPath(imagePath: url) { (image) in
if let updateCell = tableView.cellForRow(at: indexPath) {
updateCell.imageView?.image = image
tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.none)
}
}
}
return cell
}
Imageloader:
class ImageLoader {
var imageCache = NSCache<NSString, UIImage>()
init() {
self.imageCache = NSCache()
}
func obtainImageWithPath(imagePath: URL, completionHandler: #escaping (UIImage) -> ()) {
if let image = self.imageCache.object(forKey: imagePath.absoluteString as NSString) {
DispatchQueue.main.async {
completionHandler(image)
}
} else {
let placeholder: UIImage = UIImage(named: "placeholderImage")!
DispatchQueue.main.async {
completionHandler(placeholder)
}
let task = URLSession.shared.dataTask(with: imagePath, completionHandler: {data, response, error in
let image: UIImage! = UIImage(data: data!)
self.imageCache.setObject(image, forKey: imagePath.absoluteString as NSString)
DispatchQueue.main.async {
completionHandler(image)
}
})
task.resume()
}
}
}
Update your ImageLoader code to let you check if an image is already in the cache and return it synchronously to you.
Then when you load the cell, if it has the image, set it immediately. If it doesn't have the image, have the completion handler instead do a reload on the cell at that index path. Then, since the image is now cached, your regular cell loading code will be able to populate the image. Just make sure you only request the image if it wasn't already set from the cache.
The way your code is written right now, whether it set an image from the cache or not in your completion handler, it's endlessly trying to reload the row that it just set the image on, which is also going to impact performance. Hence why, as I said, you should only reload the cell if a new image was just downloaded. And don't set the image in the completion handler, just reload the row.

Why URL not working correctly when tried to turn image?

I have question about firebase. I have image's URL in my database and I show them in collectionView cells but in this point I have some problem which is some image not loaded correctly their URLs are different but images are same. I tried lots of things but I can't solve it. My URLs starting with 'https' and App Transport Security Setting, Allow Arbitrary Loads = YES. So these are not solved my problem. Here my code which are firebase and adding this URL to imageViews. Please help me! Thanks!
func firebaseCon() {
let ref = Database.database().reference().child("cells")
ref.observe(.childAdded) { (snapshot) in
if let dict = snapshot.value as? [String: AnyObject] {
let dataCon = ItemCellImage()
dataCon.itemImageName = dict["itemimagename"] as? String
dataCon.itemTitleLabel = dict["itemimagelabel"] as? String
//print(dataCon.itemImageName, dataCon.itemTitleLabel)
self.itemler.append(dataCon)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
}
}
And here from data to image:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! itemsCell
let dataGelen = itemler[indexPath.row]
cell.titleLabel.text = dataGelen.itemTitleLabel
if let cellDataImage = dataGelen.itemImageName {
let url = URL(string: cellDataImage)
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
cell.itemsimageView.image = UIImage(data: data!)
self.imageDeneme = UIImage(data: data!)!
}
}.resume()
}
return cell
}
When I tried to change image url from firebase some of them working correctly and some of them show just previous image. Is there any way to show exactly correct image in every change?
For this kind of problem due to Reusing concept you can use prepareForReuse() function provide by cells.
override func prepareForReuse() {
yourImageView.image = nil //Or any placeholder image
}
This function is called when OS is about to reuse your cell, thus you can use it as a kind of reset to your cell appearance.

Swift 4 program showing data after downloading images but data is not related

I am making a swift app where i am downloading data from API. which gives a JSON.And from there i am putting image url in an imageArray and movieTiteUrl in movieTitleArray. but when i am showing them to collection view they are showing data but that data is not related. To download images i am using AlamofireImage Below code will help you to understand my problem better.
inside ViewDidLoad
var imageUrlArray = [String]()
var imageArray = [UIImage]()
var movieTitleArray = [String]()
UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "movieCell", for: indexPath) as? MovieCell else { return UICollectionViewCell() }
cell.movieName.image = imageArray[indexPath.row]
cell.movieNameLbl.text = movieTitleArray[indexPath.row]
return cell
}
An extension which download data and download images
func downloadImages(handler: #escaping (_ status: Bool)-> ()){
imageArray = []
movieTitleArray = []
for url in imageUrlArray{
Alamofire.request(url).responseImage(completionHandler: { (response) in
guard let image = response.result.value else { return }
self.imageArray.append(image)
if self.imageArray.count == self.imageUrlArray.count {
handler(true)
}
})
}
}
func retriveData(handler : #escaping (_ status: Bool) -> ()){
print(getPopularMovies(pageNumber: 1))
Alamofire.request(getPopularMovies(pageNumber: 1)).responseJSON { (response) in
guard let json = response.result.value as? Dictionary<String, AnyObject> else { return }
let dataDictArray = json["results"] as! [Dictionary<String, AnyObject>]
for data in dataDictArray {
guard let imageUrl = data["poster_path"] as? String else { return }
guard let name = data["original_title"] as? String else { return }
let updatedImageUrl = getFullImageUrl(imageUrl: imageUrl)
self.imageUrlArray.append(updatedImageUrl)
self.movieTitleArray.append(name)
}
handler(true)
}
}
func updateDataToCollectionView(){
retriveData{(finished) in
if finished{
self.downloadImages(handler: { (finishedDownloadingImage) in
if finishedDownloadingImage{
self.movieCollectionView.reloadData()
}
})
}
}
}
I have found two observation ( problems ).
As you are using Async request to download the images, it is not guaranteed that you will get the response in the requested order. for example, if you request movie1Image,movie2Image....it is not guarntee that first you will receive movie1Image and the movie2Image in the same order. So definitely it is not reliable. use Dictionary to solve this. [String:Data] string -> ImageUrl, Data -> ImageData
Why did you wait till all images get downloaded? any specific scenario or any business requirement. Ideally, for a greater User Experience and for interaction, it is not recommended to wait till all the images get downloaded.

I am trying to fetch asset using local identifier in collection view using dispatch background but it takes too much time to load and cell are empty

I am trying to load asset from local identifier using fetch assets from local identifier in cell for item
at indexpath in background thread and displaying it in collection view cells but it takes too much time to load and cells are nil.there are hundred of images.here is the code.
does this approach is correct for fetching assets in cellfor indexpath??
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GridCell", for: indexPath) as! GridCell
cell.representedAssetIdentifier = "localidentifier"
DispatchQueue.main.async() {
if let cachedImage = self.imageCache.object(forKey: localidentifier! as NSString) as? UIImage {
cell.image = cachedImage
}
}
//let asset = self.assetsByDate[indexPath.section].1[indexPath.row]
DispatchQueue.global(qos: .background).async {
let asset = PHAsset.fetchAssets(withLocalIdentifiers: [localidentifier!], options: .none).firstObject
let mystring = String(describing: asset)
//print("the asset to string \(mystring)")
// print ("String to asset \( mystring as! PHAsset)")
self.imageManager.requestImage(for: asset!, targetSize: self.thumbnailSize, contentMode: .aspectFit, options: PHImageRequestOptions(), resultHandler: { result, info in
if cell.representedAssetIdentifier =="localidentifier" {
DispatchQueue.main.async() {
DispatchQueue.main.async() {
if let image = result {
self.imageCache.setObject(image, forKey:localidentifier! as NSString)
cell.image= image
}
}
}
}
})
}
Even if you load image in background thread (and that's a good point), the cell shouldn't be nil. So I think the probleme is not exactly there.
In your case the method should return a cell with an ImageView even if the image is still loading.
Then, when loading will be ended, it should display the image in the imageView.
The probleme may also come from the duplicate line about main thread :
DispatchQueue.main.async() {
DispatchQueue.main.async() {
You should remove one of them.
You also have to use a weak reference to avoid nil exception or leak:
DispatchQueue.main.async() { [weak self] in
if let image = result {
self.imageCache.setObject(image, forKey:localidentifier! as NSString)
cell.image= image
}
}

Downloading Image from Api and populate Image in CollectionViewController using Swift

I am able to find the response data in array but not able to download and populate in Collection View. I have tried to upload the image from the image container from application but not able to download and upload by API
Code
func get_data_from_url(){
//API calls
let url = NSURL(string: "http://android.eposapi.co.uk/?app_id=A1A2A3A4&app_key=K1K2K3K4&request=gallery")
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "POST"
let postString = "gallery"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request){
data,response,error in
if error != nil{
print("error=\(error)")
return
}
//Print out response object
print("response= \(response)")
//print response body
// let responseString = NSString(data: data!, encoding: NSUTF8StringEncoding)
// print("response data = \(responseString!)")
var json: NSArray!
do {
json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions()) as? NSArray
print(json)
print(json[0])
} catch {
print(error)
}
dispatch_async(dispatch_get_main_queue()) {
self.collectionView!.reloadData()
}
}
task.resume()
override func viewDidLoad() {
super.viewDidLoad()
get_data_from_url()
self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as UICollectionViewCell
// Configure the cell
let image = cell.viewWithTag(1) as! UIImageView
image.image = images[indexPath.row]
return cell
}
Tell me any one solution how to download images of anytype (either png or jpg) and populate in the UICollectionView using mutable urlRequest/
I have done exactly this recently, it is a Memory game that fetches images from instagram and displays the images in a UICollectionView. Please checkout the SwiftIntro project on Github
I fetch images using Alamofire:
func prefetchImages(urls: [URLRequestConvertible], done: Closure) {
imageDownloader.downloadImages(URLRequests: urls) {
response in
done()
}
}
This is a "prefetch" solution, then I can retrieve the images using this function:
func imageFromCache(url: NSURL) -> UIImage? {
guard let cache = imageCache else { return nil }
let imageFromCache = cache.imageForRequest(NSURLRequest(URL: url), withAdditionalIdentifier: nil)
return imageFromCache
}
Checkout the ImagePrefetcher class.
The MemoryDataSourceClass which implements the UICollectionViewDataSource and UICollectionViewDelegate protocols returns UICollectionViewCells of type CardCVCell which contains an UIImageView created in its .Xib. I set the image on the UIImageView in this method:
func updateWithModel(model: Model) {
guard let card = model as? CardModel else { return }
guard let cachedImage = ImagePrefetcher.sharedInstance.imageFromCache(card.imageUrl) else { return }
cardFrontImageView.image = cachedImage
flipped = card.flipped
}
Sorry about the ImagePrefetcher.sharedInstance Singleton ;), Singletons are bad (as discussed here and here)! I have not yet set up Dependency Injection using amazing Swinject, but will do so soon! :)
Try another way like NSURLSession to download the data
var url: NSURL = NSURL(string:"http://android.eposapi.co.uk/?app_id=A1A2A3A4&app_key=K1K2K3K4&request=gallery")!
var downloadPhotoTask: NSURLSessionDownloadTask =
NSURLSession.sharedSession().downloadTaskWithURL(url, completionHandler: {(location: NSURL, response: NSURLResponse, error: NSError) -> Void in
var downloadedImage: UIImage = UIImage.imageWithData(NSData.dataWithContentsOfURL(location))
})
downloadPhotoTask.resume()