I have a tableview that populates its cells with data from firebase. Each cell posses an image and each image is loaded asynchronously and cached. The problem is that random cells will use the wrong image, although the other data in the cell is correct. If I scroll so that the cell goes off view, the correct image then loads.
Research:
Images in UITableViewCells are loading wrong (Dont really understand Obj C)
What is the correct way to use prepareForReuse?
Image Cache:
import Foundation
import UIKit
let imageCache = NSCache<NSString, AnyObject>()
extension UIImageView {
func loadImageUsingCache(urlString: String) {
self.image = #imageLiteral(resourceName: "logo3")
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
}
Tableview Cell
import Foundation
import Firebase
class GroupCell: UITableViewCell {
var group: Groups!
var members = [String]()
#IBOutlet weak var groupImage: UIImageView!
#IBOutlet weak var groupName: UILabel!
#IBOutlet weak var groupRep: UILabel!
#IBOutlet weak var memberCount: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
func configureCell(group: Groups) {
self.group = group
self.groupName.text = group.name
self.groupRep.text = "\(group.groupScore)"
if let groupImage = group.groupImage {
self.groupImage.loadImageUsingCache(urlString: groupImage)
} else {
self.groupImage.image = //random image
}
for member in group.members {
self.members.append(member.key)
}
self.memberCount.text = "\(members.count)"
}
}
TableView
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "groupCell") as? GroupCell {
let group = FriendSystem.system.activeGroups[indexPath.row]
cell.configureCell(group: group)
return cell
}
return UITableViewCell()
}
Sounds like you need to add the prepareForReuse method to your GroupCell class.
In that method add self.groupImage.image = nil It will reset your image view to empty until the correct image is set.
Related
I have the following code for my tableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
// Reset the image in the cell
cell.coverImageView.image = nil
// Get the recipe that the tableView is asking about
let recipeInTable = recipe[indexPath.row]
cell.displayRecipe(recipe: recipeInTable, indexPathRow: indexPath.row)
return cell
}
This is my custom cell class, where I am caching the image data to pull from:
class CustomCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var coverImageView: UIImageView!
var recipeToDisplay:Recipe?
var recipeToPullFrom:Recipe!
func displayRecipe(recipe:Recipe, indexPathRow:Int) {
recipeToPullFrom = recipe
DispatchQueue.main.async {
self.titleLabel.text = self.recipeToPullFrom.title
if self.recipeToPullFrom.image == nil {
return
}
else {
let urlString = self.recipeToPullFrom.image
if let imageData = CacheManager.retrieveData(urlString!) {
self.coverImageView.image = UIImage(data: imageData)
return
}
let url = URL(string: urlString!)
guard url != nil else {
print("Could not create url object")
return
}
let session = URLSession.shared
let dataTask = session.dataTask(with: url!) { (data, response, error) in
if error == nil && data != nil {
CacheManager.saveData(urlString!, data!)
if self.recipeToPullFrom.image == urlString {
DispatchQueue.main.async {
// Display the image data in the imageView
self.coverImageView.image = UIImage(data: data!)
}
}
DispatchQueue.main.async {
self.coverImageView.image = UIImage(data: data!)
}
} // End if
} // End dataTask
// Kick off the dataTask
dataTask.resume()
}
}
}
}
And finally my Cache Manager:
class CacheManager {
static var imageDictionary = [String:Data]()
static func saveData(_ url:String, _ imageData:Data) {
// Save the image data along with the url
imageDictionary[url] = imageData
}
static func retrieveData(_ url:String) -> Data? {
// Return the saved imageData or nil
return imageDictionary[url]
}
}
From what I've researched, adding the following in my tableView function should have reset the image in my cell before inputting a new one: cell.coverImageView.image = nil.
Is there something I'm missing? I've also noticed that only my images are showing in the wrong cells. Could I be doing something incorrect with retrieving the image data from cache?
Any direction or support is much appreciated!
Since your images are downloaded asynchronously then it can be downloaded after the cell been reused for another object. What you can do:
When your image is ready to be displayed (downloaded) you need to check if it should be displayed in that cell.
Or
You can also hold a reference to the URLSessionDataTask and cancel the it before reusing the cell by overriding the prepareForReuse method.
I don't think that you need here an asynchronously call especially, when you using .main in other .main. But If you what so you what so make some thing like
DispatchQueue.global(qos: .userInteractive).async {
// Never do here ui code - label, view, images.. anything; or would crash
DispatchQueue.main.async {
// To do here you ui updates
}
}
here is some refactored code with reuse method. I agree with
Hach3m. U need to cancel requests if you don't it anymore, when next cell is about to shown
class CustomCell: UITableViewCell {
#IBOutlet private weak var titleLabel: UILabel!
#IBOutlet private weak var coverImageView: UIImageView!
private var dataTask: URLSessionDataTask?
func displayRecipe(recipe: Recipe, indexPathRow: Int) {
titleLabel.text = recipe.title enter code here
guard let urlString = recipe.image else { return }
if let imageData = CacheManager.retrieveData(urlString) {
coverImageView.image = UIImage(data: imageData)
return
}
guard let url = URL(string: urlString) else { return print("Could not create url object") }
dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return self.clear() }
CacheManager.saveData(urlString, data)
self.coverImageView.image = UIImage(data: data)
}
dataTask?.resume()
}
override func prepareForReuse() {
super.prepareForReuse()
clear()
}
private func clear() {
dataTask?.cancel()
dataTask = nil
}
}
}
If someone have good idea to implement this let me know
thanks in adance
class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var tableViewCell: UITableViewCell!
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingCell", for: indexPath)
print(self.array[indexPath.row])
cell.textLabel?.text = (self.array[indexPath.row]["title"] as! String)
cell.detailTextLabel?.text = (self.array[indexPath.row]["username"] as! String)
Alamofire.request(imageUrl!, method: .get).response { response in
guard let image = UIImage(data:response.data!) else {
// Handle error
return
}
let imageData = UIImageJPEGRepresentation(image,1.0)
cell.myImage.image = UIImage(data : imageData!)
}
return cell
}
}
You are force unwrapping some values. Try to safely unwrap the values instead. You still have to figure out why the values are nil though.
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
if let url = NSURL(string: self.restaurants[indexPath.row].restaurantImage), let data = NSData(contentsOfURL: url), let image = UIImage(data: data) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
cell.restaurantImage.image = image
}
}
})
This will help you figure out where the exact issue is
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
guard indexPath.row < self.restaurants.count
else
{
print("Indexpath greater than restaurants count")
return
}
guard let imageUrl = NSURL(string:self.restaurants[indexPath.row].restaurantImage)
else
{
print("Image url is empty")
return
}
guard let imageData = NSData(contentsOfURL: imageUrl)
else
{
print("Cannot retreive data at url")
return
}
guard let image = UIImage(data: imageData)
else
{
print("Data doesnt contain image")
return
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
cell.restaurantImage.image = image
}
})
I'm having a very odd issue with my app, and all I can think of is that it is some sort of caching issue. Basically, when I scroll my UITableView, the thumbnails that are shown are being reloaded multiple times for the same row, typically with different images each time, before finally landing on the right image.
A short (20 second) screen capture is here: https://youtu.be/oa04mlOgMeQ
The app should be caching these once loaded, and the images are named to match the bonus codes (and the name is in the JSON file). I can't figure out why it is doing this.
EDIT: Here is my cellForRowAt code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "BonusListViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BonusListViewCell else {
fatalError("The dequeued cell is not an instance of BonusListViewCell.")
}
// let bonus = bonuses[indexPath.row]
let bonus: JsonFile.JsonBonuses
if isFiltering() {
bonus = filteredBonuses[indexPath.row]
} else {
bonus = bonuses[indexPath.row]
}
let urlString = "http://tourofhonor.com/appimages/"+(bonus.imageName)
let url = URL(string: urlString)
cell.primaryImage.downloadedFrom(url: url!)
cell.nameLabel.text = bonus.name.capitalized
cell.bonusCodeLabel.text = bonus.bonusCode.localizedUppercase
cell.categoryLabel.text = bonus.category
cell.valueLabel.text = "\(bonus.value)"
cell.cityLabel.text = "\(bonus.city.capitalized),"
cell.stateLabel.text = bonus.state.localizedUppercase
return cell
}
Here is my downloadedFrom function:
extension UIImageView {
func downloadedFrom(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit) {
contentMode = mode
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.main.async() {
self.image = image
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloadedFrom(url: url, contentMode: mode)
}
}
My UITableViewCell code is:
import UIKit
class BonusListViewCell: UITableViewCell {
//MARK: Properties
#IBOutlet weak var bonusCodeLabel: UILabel!
#IBOutlet weak var categoryLabel: UILabel!
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var valueLabel: UILabel!
#IBOutlet weak var cityLabel: UILabel!
#IBOutlet weak var stateLabel: UILabel!
#IBOutlet weak var flavorText: UITextView!
#IBOutlet weak var primaryImage: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "BonusListViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BonusListViewCell else {
fatalError("The dequeued cell is not an instance of BonusListViewCell.")
}
// let bonus = bonuses[indexPath.row]
let bonus: JsonFile.JsonBonuses
if isFiltering() {
bonus = filteredBonuses[indexPath.row]
} else {
bonus = bonuses[indexPath.row]
}
let urlString = "http://tourofhonor.com/appimages/"+(bonus.imageName)
let url = URL(string: urlString)
//set image url
cell.imageUrl = URL(string: images[indexPath.row])
cell.nameLabel.text = bonus.name.capitalized
cell.bonusCodeLabel.text = bonus.bonusCode.localizedUppercase
cell.categoryLabel.text = bonus.category
cell.valueLabel.text = "\(bonus.value)"
cell.cityLabel.text = "\(bonus.city.capitalized),"
cell.stateLabel.text = bonus.state.localizedUppercase
return cell
}
In your cell
import UIKit
class BonusListViewCell: UITableViewCell {
//MARK: Properties
#IBOutlet weak var bonusCodeLabel: UILabel!
#IBOutlet weak var categoryLabel: UILabel!
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var valueLabel: UILabel!
#IBOutlet weak var cityLabel: UILabel!
#IBOutlet weak var stateLabel: UILabel!
#IBOutlet weak var flavorText: UITextView!
#IBOutlet weak var primaryImage: UIImageView!
#IBOutlet weak var activity: UIActivityIndicatorView!
var imageUrl: URL? {
didSet {
loadImage()
}
}
var task: URLSessionDataTask?
override func awakeFromNib() {
super.awakeFromNib()
clean()
}
func loadImage() {
//you need cancel previous task
task?.cancel()
clean()
guard let imageUrl = imageUrl else {
return
}
activity.startAnimating()
task = URLSession.shared.dataTask(with: imageUrl) { [weak self] data, response, error in
DispatchQueue.main.async {
self?.activity.stopAnimating()
if error == nil, let data = data {
self?.primaryImage.image = UIImage(data: data)
}
}
}
task?.resume()
}
func clean() {
primaryImage.image = nil
}
}
But it's better to use libraries like Alamofire or SDWebImage
Your problem is that previously dispatched image fetches are completing after the cell has been reused, resulting in outdated images appearing, in turn, until the final, correct, image is loaded.
There are existing frameworks available that will make this easier for you and also perform caching, but you may not want to introduce 3rd party code into your app.
One approach is to use a UIImageView subclass rather than an extension so that you can add a property for the current url and an in-memory cache:
class LoadableImageView: UIImageView {
private var currentURL: URL?
private static imageCache = NSCache<NSURL,UIImage>()
private static cacheUpdateQueue = DispatchQueue.global(qos: .userInitiated)
func downloadedFrom(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit) {
contentMode = mode
self.currentURL = url
if let image = LoadableImageView.imageCache.object(forKey:url as NSURL) {
self.image = image
return
}
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),
url == self.currentURL
else { return }
LoadableImageView.cacheUpdateQueue.sync {
LoadableImageView.imageCache.setObject(image, forKey: url as NSURL)
}
DispatchQueue.main.async() {
self.image = image
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloadedFrom(url: url, contentMode: mode)
}
}
You will need to set an appropriate placeholder image or nil in prepareForReuse or cellForRowAt
I know there are many answers about this error but I cannot seem to figure it out. I am receiving the error Thread 1: EXC_BAD_ACCESS (code=257, address=0x200000003) on the line where the function is called. My table cell view controller is as follows.
import UIKit
class NewsTableViewCell: UITableViewCell {
#IBOutlet weak var postImageView: CustomImageView!
#IBOutlet weak var postTitleLabel:UILabel!
#IBOutlet weak var authorLabel:UILabel!
#IBOutlet weak var dateLabel: UILabel!
var article: Article? {
didSet {
postTitleLabel.text = article?.title
authorLabel.text = article?.author
dateLabel.text = article?.date
setupArticleImage()
}
}
func setupArticleImage() {
postImageView.loadImageUsingUrlString("http://theblakebeat.com/uploads/873463.jpg")
}
This code calls the function loadImageUsingUrlString, which is located in my extensions.swift file. It is called for each table view cell that is loaded in order to load its image. The code for extensions.swift is as follows.
import UIKit
let imageCache = NSCache()
class CustomImageView: UIImageView {
var imageUrlString: String?
func loadImageUsingUrlString(urlString: String) {
imageUrlString = urlString
let url = NSURL(string: urlString)
image = nil
if let imageFromCache = imageCache.objectForKey(urlString) as? UIImage {
self.image = imageFromCache
return
}
NSURLSession.sharedSession().dataTaskWithURL(url!, completionHandler: { (data, respones, error) in
if error != nil {
print(error)
return
}
dispatch_async(dispatch_get_main_queue(), {
let imageToCache = UIImage(data: data!)
if self.imageUrlString == urlString {
self.image = imageToCache
}
imageCache.setObject(imageToCache!, forKey: urlString)
})
}).resume()
}
}
Thank you in advance for any help.
You did not set the class CustomImageView to the postImageView in the IBInspector:
Your extensions.swift is in the right Target?
In XCode select the file in Project Navigator and check in File Inspector the section Target Membership
I'm trying to async load pictures inside my FriendsTableView (UITableView) cell. The images load fine but when I'll scroll the table the images will change a few times and wrong images are getting assigned to wrong cells.
I've tried all methods I could find in StackOverflow including adding a tag to the raw and then checking it but that didn't work. I'm also verifying the cell that should update with indexPath and check if the cell exists. So I have no idea why this is happening.
Here is my code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("friendCell", forIndexPath: indexPath) as! FriendTableViewCell
var avatar_url: NSURL
let friend = sortedFriends[indexPath.row]
//Style the cell image to be round
cell.friendAvatar.layer.cornerRadius = 36
cell.friendAvatar.layer.masksToBounds = true
//Load friend photo asyncronisly
avatar_url = NSURL(string: String(friend["friend_photo_url"]))!
if avatar_url != "" {
getDataFromUrl(avatar_url) { (data, response, error) in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
let thisCell = tableView.cellForRowAtIndexPath(indexPath)
if (thisCell) != nil {
let updateCell = thisCell as! FriendTableViewCell
updateCell.friendAvatar.image = UIImage(data: data)
}
}
}
}
cell.friendNameLabel.text = friend["friend_name"].string
cell.friendHealthPoints.text = String(friend["friend_health_points"])
return cell
}
On cellForRowAtIndexPath:
1) Assign an index value to your custom cell. For instance,
cell.tag = indexPath.row
2) On main thread, before assigning the image, check if the image belongs the corresponding cell by matching it with the tag.
dispatch_async(dispatch_get_main_queue(), ^{
if(cell.tag == indexPath.row) {
UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
thumbnailImageView.image = tmpImage;
}});
});
This is because UITableView reuses cells. Loading them in this way causes the async requests to return at different time and mess up the order.
I suggest that you use some library which would make your life easier like Kingfisher. It will download and cache images for you. Also you wouldn't have to worry about async calls.
https://github.com/onevcat/Kingfisher
Your code with it would look something like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("friendCell", forIndexPath: indexPath) as! FriendTableViewCell
var avatar_url: NSURL
let friend = sortedFriends[indexPath.row]
//Style the cell image to be round
cell.friendAvatar.layer.cornerRadius = 36
cell.friendAvatar.layer.masksToBounds = true
//Load friend photo asyncronisly
avatar_url = NSURL(string: String(friend["friend_photo_url"]))!
if avatar_url != "" {
cell.friendAvatar.kf_setImageWithURL(avatar_url)
}
cell.friendNameLabel.text = friend["friend_name"].string
cell.friendHealthPoints.text = String(friend["friend_health_points"])
return cell
}
UPDATE
There are some great open source libraries for image caching such as KingFisher and SDWebImage. I would recommend that you try one of them rather than writing your own implementation.
END UPDATE
So there are several things you need to do in order for this to work. First let's look at the caching code.
// Global variable or stored in a singleton / top level object (Ex: AppCoordinator, AppDelegate)
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String) -> URLSessionDataTask? {
guard let url = URL(string: imgURL) else { return nil }
// set initial image to nil so it doesn't use the image from a reused cell
image = nil
// check if the image is already in the cache
if let imageToCache = imageCache.object(forKey: imgURL as NSString) {
self.image = imageToCache
return nil
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print(err)
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
imageCache.setObject(imageToCache!, forKey: imgURL as NSString)
self.image = imageToCache
}
}
task.resume()
return task
}
}
You can use this outside of a TableView or CollectionView cell like this
let imageView = UIImageView()
let imageTask = imageView.downloadImage(from: "https://unsplash.com/photos/cssvEZacHvQ")
To use this in a TableView or CollectionView cell you'll need to reset the image to nil in prepareForReuse and cancel the download task. (Thanks for pointing that out #rob
final class ImageCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
private var task: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
task = nil
imageView.image = nil
}
// Called in cellForRowAt / cellForItemAt
func configureWith(urlString: String) {
if task == nil {
// Ignore calls when reloading
task = imageView.downloadImage(from: urlString)
}
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "imageCell", for: indexPath) as! ImageCell
cell.configureWith(urlString: "https://unsplash.com/photos/cssvEZacHvQ") // Url for indexPath
return cell
}
Keep in mind that even if you use a 3rd party library you'll still want to nil out the image and cancel the task in prepareForReuse
If targeting iOS 13 or later, you can use Combine and dataTaskPublisher(for:). See WWDC 2019 video Advances in Networking, Part 1.
The idea is to let the cell keep track of the “publisher”, and have prepareForReuse:
cancel the prior image request;
set the image property of the image view to nil (or a placeholder); and then
start another image request.
For example:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let url = ...
cell.setImage(to: url)
return cell
}
}
class CustomCell: UITableViewCell {
#IBOutlet weak var customImageView: UIImageView!
private var subscriber: AnyCancellable?
override func prepareForReuse() {
super.prepareForReuse()
subscriber?.cancel()
customImageView?.image = nil
}
func setImage(to url: URL) {
subscriber = ImageManager.shared.imagePublisher(for: url, errorImage: UIImage(systemName: "xmark.octagon"))
.assign(to: \.customImageView.image, on: self)
}
}
Where:
class ImageManager {
static let shared = ImageManager()
private init() { }
private let session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .returnCacheDataElseLoad
let session = URLSession(configuration: configuration)
return session
}()
enum ImageManagerError: Error {
case invalidResponse
}
func imagePublisher(for url: URL, errorImage: UIImage? = nil) -> AnyPublisher<UIImage?, Never> {
session.dataTaskPublisher(for: url)
.tryMap { data, response in
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode,
let image = UIImage(data: data)
else {
throw ImageManagerError.invalidResponse
}
return image
}
.replaceError(with: errorImage)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
If targeting earlier iOS versions, rather than using Combine, you can use URLSession, with the same idea of canceling the prior request in prepareForReuse:
class CustomCell: UITableViewCell {
#IBOutlet weak var customImageView: UIImageView!
private weak var task: URLSessionTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
customImageView?.image = nil
}
func setImage(to url: URL) {
task = ImageManager.shared.imageTask(for: url) { result in
switch result {
case .failure(let error): print(error)
case .success(let image): self.customImageView.image = image
}
}
}
}
Where:
class ImageManager {
static let shared = ImageManager()
private init() { }
private let session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .returnCacheDataElseLoad
let session = URLSession(configuration: configuration)
return session
}()
enum ImageManagerError: Error {
case invalidResponse
}
#discardableResult
func imageTask(for url: URL, completion: #escaping (Result<UIImage, Error>) -> Void) -> URLSessionTask {
let task = session.dataTask(with: url) { data, response, error in
guard let data = data else {
DispatchQueue.main.async { completion(.failure(error!)) }
return
}
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode,
let image = UIImage(data: data)
else {
DispatchQueue.main.async { completion(.failure(ImageManagerError.invalidResponse)) }
return
}
DispatchQueue.main.async { completion(.success(image)) }
}
task.resume()
return task
}
}
Depending on the implementation there can be many things that will cause all of the answers here to not work (including mine). Checking the tag did not work for me, checking the cache neither, i have a custom Photo class that carries the full image, thumbnail and more data, so i have to take care of that too and not just prevent the image from being reused improperly. Since you will probably be assigning the images to the cell imageView after they're done downloading, you will need to cancel the download and reset anything you need on prepareForReuse()
Example if you're using something like SDWebImage
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.sd_cancelCurrentImageLoad()
self.imageView = nil
//Stop or reset anything else that is needed here
}
If you have subclassed the imageview and handle the download yourself make sure you setup a way to cancel the download before the completion is called and call the cancel on prepareForReuse()
e.g.
imageView.cancelDownload()
You can cancel this from the UIViewController too. This on itself or combined with some of the answers will most likely solve this issue.
I solve the problem just implementing a custom UIImage class and I did a String condition as the code below:
let imageCache = NSCache<NSString, UIImage>()
class CustomImageView: UIImageView {
var imageUrlString: String?
func downloadImageFrom(withUrl urlString : String) {
imageUrlString = urlString
let url = URL(string: urlString)
self.image = nil
if let cachedImage = imageCache.object(forKey: urlString as NSString) {
self.image = cachedImage
return
}
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
if let image = UIImage(data: data!) {
imageCache.setObject(image, forKey: NSString(string: urlString))
if self.imageUrlString == urlString {
self.image = image
}
}
}
}).resume()
}
}
It works for me.
TableView reuses cells. Try this:
import UIKit
class CustomViewCell: UITableViewCell {
#IBOutlet weak var imageView: UIImageView!
private var task: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
imageView.image = nil
}
func configureWith(url string: String) {
guard let url = URL(string: string) else { return }
task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
task?.resume()
}
}
Because TableView reuses cells. In your cell class try this code:
class CustomViewCell: UITableViewCell {
#IBOutlet weak var catImageView: UIImageView!
private var task: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
catImageView.image = nil
}
func configureWith(url string: String) {
guard let url = URL(string: string) else { return }
task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.catImageView.image = image
}
}
}
task?.resume()
}
}
the Best Solution for This Problem i have is for Swift 3 or Swift 4
Simply write these two lines
cell.videoImage.image = nil
cell.thumbnailimage.setImageWith(imageurl!)
Swift 3
DispatchQueue.main.async(execute: {() -> Void in
if cell.tag == indexPath.row {
var tmpImage = UIImage(data: imgData)
thumbnailImageView.image = tmpImage
}
})
I created a new UIImage variable in my model and load the image/placeholder from there when creating a new model instance. It worked perfectly fine.
It is an example that using Kingfisher caching at memory and disk after downloaded.
It replace UrlSession downloading traditional and avoid re-download UIImageView after scroll down TableViewCell
https://gist.github.com/andreconghau/4c3b04205195f452800d2892e91a079a
Example Output
sucess
Image Size:
(460.0, 460.0)
Cache:
disk
Source:
network(Kingfisher.ImageResource(cacheKey: "https://avatars0.githubusercontent.com/u/5936?v=4", downloadURL: https://avatars0.githubusercontent.com/u/5936?v=4))
Original source:
network(Kingfisher.ImageResource(cacheKey: "https://avatars0.githubusercontent.com/u/5936?v=4", downloadURL: https://avatars0.githubusercontent.com/u/5936?v=4))