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.
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
I have a tableView that has an imageView as a background, of which the image is either found in the Documents Directory, an NSCache, or downloaded from Firebase if it isn't found on the device. When downloading from Firebase there is no freezing, however when the cell is loading the image from the device, the scrolling freezes until it loads the image as the background image. I can't figure out why. Heres the code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? TripCell
let trip = trips[indexPath.row]
let path = getDocumentsDirectory().appendingPathComponent(trip.coverUID)
if var image = UIImage(contentsOfFile: path) {
image = image.resizedImage(newSize: CGSize(width: (image.size.width)/2, height: (image.size.height)/2))
cell?.updateUI(trip: trip, image: image)
print("Loaded from DocumentsDirectory")
} else if let data = TripsVC.imageCache.object(forKey: trip.coverUID as NSString) {
//This is mainly where it freezes
var cachedImage = UIImage(data: data as Data)
cachedImage = cachedImage?.resizedImage(newSize: CGSize(width: (cachedImage?.size.width)!/1.5, height: (cachedImage?.size.height)!/1.5))
cell?.updateUI(trip: trip, image: cachedImage)
} else {
cell?.updateUI(trip: trip)
}
return cell!
}
func updateUI(trip: Trip, image: UIImage? = nil) {
self.trip = trip
if image != nil {
print("Loaded from device")
backgroundImage.image = image
setLabels()
} else {
//Doesn't freeze here
let url = trip.coverPhotoUrl
let ref = FIRStorage.storage().reference(forURL: url)
ref.data(withMaxSize: 5*1024*1024, completion: { [weak self]
(data, error) in
if error != nil {
print("Couldn't download image")
} else {
print("Image downloaded from storage")
if let imageData = data {
if var image = UIImage(data: imageData) {
image = image.resizedImage(newSize: CGSize(width: (image.size.width)/8, height: (image.size.height)/8))
self?.backgroundImage.image = image
self?.setLabels()
TripsVC.imageCache.setObject(imageData as NSData, forKey: trip.coverUID as NSString)
}
}
}
})
}
}
Good day! It makes me mad, 'cos I do not understand what's going on.
I've got 2 TableViewControllers with some similar logics.
I work with 1 Data Model. This model contains user info and each entity always has a photo. To init it, I use the code below :
var partnerPhoto: UIImage? = nil
if let imageData = partners[indexPath.item].userPhoto {
partnerPhoto = UIImage(data: imageData as! Data)
}
In debug partners[indexPath.item].userPhoto has real data and even imageData shows 4213 bytes.But, app crashes with typical error:
fatal error: unexpectedly found nil while unwrapping an Optional value
EDIT:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if dialogs[indexPath.item].fromID == profileID {
let cell = tableView.dequeueReusableCell(withIdentifier: "dialogMeCell", for: indexPath) as! CellForDialogMe
var partnerPhoto: UIImage? = nil
if let imageData = partners[indexPath.item].userPhoto {
partnerPhoto = UIImage(data: imageData as! Data)
}
cell.fillWithContent(partnerPhoto: partnerPhoto!, selfPhoto: profilePhoto!)
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "dialogHimCell", for: indexPath) as! CellForDialogHim
var partnerPhoto: UIImage? = nil
if let imageData = partners[indexPath.item].userPhoto {
partnerPhoto = UIImage(data: imageData as! Data)
}
cell.fillWithContent(partnerPhoto: partnerPhoto!)
return cell
}
SOLUTION:
In fact I have not found concrete solution for this problem, I have just moved some logic from model to view controller. In model I did load from URL to data. Now I do it right in ViewController and it works perfect, but it is odd, very odd for me, 'cos I just did cmd-x cmd-v.
Cast your imageData before the scope:
if let imageData = partners[indexPath.row].userPhoto as? Data {
partnerPhoto = UIImage(data: imageData)
} else {
partnerPhoto = UIImage() //just to prevent the crash
debugPrint("imageData is incorrect")
//You can set here some kind of placeholder image instead of broken imageData here like UIImage(named: "placeholder.png")
}
cell.fillWithContent(partnerPhoto: partnerPhoto)
return cell
Also, it would be helpful if you'll provide more details - how and when profilePhoto is init-ed and
cell.fillWithContent(partnerPhoto: partnerPhoto!, selfPhoto: profilePhoto!) code.
Also, you can set breakpoint on your cell.fillWithContentand check if partnerPhoto and/or profilePhoto is nil before functions is called.
Try this:
var partnerPhoto: UIImage?
guard let imageData = partners[indexPath.item].userPhoto else {return}
guard let image = UIImage(data: imageData) else {return}
partnerPhoto = image
In my case return nil because I try to download image from FirebaseStorage by Alamofire and than save to Core Data. Into Core Data save correct and my photoData is not nil, but UIImage(data: photoData as Data) return nil.
I resolve this problem by retrieving image native method FirebaseStorage:
func retrieveUserPhotoFromStorage(imageUID: String?, completion: #escaping (Result<Data, DomainError>) -> Void) {
guard let imageID = imageUID else { return }
let storageRef = storage.reference(withPath: DatabaseHierarhyKeys.users.rawValue).child(imageID)
storageRef.getData(maxSize: Constant.maxSize) { data, error in
guard let error = error else {
if let data = data {
completion(.success(data))
return
}
completion(.failure(.domainError(value: "errorRetrieveImage data nil")))
return
}
completion(.failure(.domainError(value: "errorRetrieveImage \(error.localizedDescription)")))
}
}
Than save it to Core Data and UIImage(data: photoData as Data) already is not nil.
I have table view in which i am showing 3 cell,and depends upon the collection view cell which is working as like paging. So When i want to get the image on table view cell from dispatch_async(dispatch_get_global_queue...) some time i got the
fatal error: unexpectedly found nil while unwrapping an Optional value
My Code is:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.videoTableView .dequeueReusableCellWithIdentifier("customTableViewCell") as! CustomTableViewCell
let videoDataObj:VideoData = subVideoArray .objectAtIndex(indexPath.row) as! VideoData
cell.titleLabel.text = videoDataObj.videoTitle as String
cell.descLabel.text = videoDataObj.videoDesc as String
cell.playButton.tag = indexPath.row + (pageIndex * noOfElement)
let imageUrl = NSURL(string: videoDataObj.videoImage as String)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)){
let data = NSData(contentsOfURL: imageUrl!)
dispatch_async(dispatch_get_main_queue(), {
cell.videoImageView?.contentMode = .ScaleToFill
cell.videoImageView?.image = UIImage(data: data!)
cell.setNeedsLayout()
cell.layoutIfNeeded()
})
}
return cell
}
Screen Shot Of View Is:enter image description here
Thanks in advance
Yes !!! I review my code and got solution. I was doing the silly mistake.
Try it..
Updated Code.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)){
if let data = NSData(contentsOfURL: imageUrl!){
dispatch_async(dispatch_get_main_queue(), {
cell.videoImageView?.contentMode = .ScaleToFill
cell.videoImageView?.image = UIImage(data: data)
cell.setNeedsLayout()
cell.layoutIfNeeded()
})
}