Loading images async in tableview - swift

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.

Related

UITableViewCell shows the wrong image while images load

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

How to download image from firebase-storage to display inside collectionViewCell's Imageview(Swift)

I have uploaded an image into firebase-storage and saved the downloadURL into a key/value pair inside of my database. I've written a code that is suppose to display the image(s) inside of a collectionView, once the data has been retrieved, if the url is valid. The code is executed at cellForItemAt since the the collectionView housing the pictures is embedded inside of another collectionView(Which will be called Main or MainCV to prevent confusion).
To solve the problem, I have tried to reload the collection view's data inside of MainCV, as well as trying to testing the code on a view controller with just an ImageView(not successful).
// function to display images
private func icon(_ imageURL: String) -> UIImage {
//print("imageURL is \(imageURL)")
let url = URL(string: imageURL)
var image: UIImage?
var imageData:Data?
if url == nil {
print("URL is \(imageURL)")
return #imageLiteral(resourceName: "ic_person_outline_white_2x")
} else {
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("error")
return
}
DispatchQueue.main.async {
imageData = data
image = UIImage(data: imageData!)
}
}.resume()
return image ?? #imageLiteral(resourceName: "ic_person_outline_white_2x")
}
}
CellForItemAt block of code
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! ImageCell
let imageOption = self.imageData[indexPath.row]
cell.iconImageView.image = icon(imageOption)
return cell
//imageData is an array with empty values that is populated with database values and subsequently reloaded
}
The intended result as I said earlier is to display the images from firebaseStorage inside of the collectionView. My code does not render any errors, but always returns the default image as well as printing the imageURl which I confirmed to be the accurate http for the image I'm trying to display.
You need to learn something about asynchronous programming.
Your function returns immediately, but URLSession.shared.dataTask(with: url!) takes some time. Timeline:
image = nil
start fetching data
return image ?? defaultImage
fetching data finished (after function returned -> image data lost)
Instead of returning immediately, provide closure taking image as a parameter into your function:
private func icon(_ imageURL: String, closure: (UIImage) -> Void)
and update your code to
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("error")
closure(#imageLiteral(resourceName: "ic_person_outline_white_2x"))
}
DispatchQueue.main.async {
imageData = data
image = UIImage(data: imageData!)
closure(image)
}
}.resume()
The closure itself can be a function accepting the image as an argument and setting this image asynchronously to your collection view cell
Also, you want to provide some default or loading image before your image is loaded. Or use ActivityIndicator.
Hope this helps!

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.

TableView cells Image have images without images

I'm downloading the data from firebase and I managed to put the images to right cells but I get the images with getData and it requires file path for images and some of them does not have images on the stated path but some how I have images on every cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tabelview.dequeueReusableCell(withIdentifier: "cell") as! TableCell
cell.commentButton.tag = indexPath.row
cell.nameLabel.text = annoucements[indexPath.row].Name
cell.announcementLabel.text = annoucements[indexPath.row].Announcement
cell.annoucementTitleLabel.text = annoucements[indexPath.row].AnnoucementTitle
cell.dateLabel.text = annoucements[indexPath.row].Date
cell.selectionStyle = .none
let storageRef = Storage.storage().reference()
let islandRef = storageRef.child("Announcement").child("\(annoucements[indexPath.row].key!).jpg")
if let cachedImage = imageCache.object(forKey: annoucements[indexPath.row].key! as AnyObject) as? UIImage{
cell.someImage.image = cachedImage
return cell
}
islandRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if error != nil {
// Uh-oh, an error occurred!
} else {
print(2)
let image = UIImage(data: data!)
DispatchQueue.main.async { // Adds the images to cache
if let downloadedImage = image {
imageCache.setObject(downloadedImage, forKey: annoucements[indexPath.row].key! as AnyObject)
cell.someImage.image = downloadedImage
}
}
cell.someImage.image = image
}
}
return cell
}
In tableview cell are reused so you don't need to surprise why you are getting image on cell even though no image at specific path .
If you can observe
when you call getData method from the cell which have no image i think the case where you are getting error nothing is set neither in cell.someImage nor in imageCache
My suggestion is to if you are going to show temporary image in your cell in case no image available then you should also set that placeholder image on cache when error block executed on getData method so it won't execute again and again. and you can set nil in override prepareForReuse of your custom cell so you won't get any repeating image.
Hope it is helpful
When you tableView.dequeueReusableCell, you actually reuse previously stored cell with its data,
You have to clear the cell of its content by calling prepareForReuse in your TableCell:
override func prepareForReuse() {
super.prepareForReuse()
someImage.image = nil // or some placeholder image
}
This how I accomplished that. Note the else statement with a placeholder image:
let cell = UITableViewCell(style: .default, reuseIdentifier: "CellID")
if let pic = PageDataSource.sharedInstance.angels[indexPath.row].photo {
cell.imageView?.image = pic
cell.setNeedsLayout()
} else if let imgURL = PageDataSource.sharedInstance.angels[indexPath.row].filepath {
Storage.storage().reference(forURL: imgURL).getData(maxSize: INT64_MAX, completion: { (data, error) in
guard error == nil else {
print("error downloading: \(error!)")
return
}
// render
let img = UIImage.init(data: data!)
// store to datasource
PageDataSource.sharedInstance.angels[indexPath.row].photo = img
// display img
if cell == tableView.cellForRow(at: indexPath){
DispatchQueue.main.async {
cell.imageView?.image = img
cell.setNeedsLayout()
}
}
})
} else {
// TODO: TODO: Change Image to a proper placeholder
cell.imageView?.image = UIImage(contentsOfFile: "Angels#2x.png")
}
cell.textLabel?.text = PageDataSource.sharedInstance.angels[indexPath.row].name
return cell
}

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