error while loading images asynchronously from firebase - ios

I have been trying to load images asynchronously for quiet a while now.
When i make a new class for this and built and run the app an error appears in the runtime log
// log
Failed to set (borderWidth) user defined inspected property on ...
could not set nil as the value for the key borderWidth.
could not set nil as the value for the key cornerRadius.
It is fine with extension instead of class, the images load async but, this way I will not be able to make sure the correct image is loaded in the image view.
// cell for item at
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "subscribeCell", for: indexPath) as! subscribeViewCell
cell.userID = self.users[indexPath.row].userId
cell.profileName.text = self.users[indexPath.row].fullName
cell.profileImage.setupWithImageUrl(imageUrl: self.users[indexPath.row].ImagePath)
checkFollowers(indexPath : indexPath)
return cell
}
the class i have been working with in the same view controller :
Class to download image:
private var imageCache = [String:UIImage]()
class CustomImageView : UIImageView {
var lastImageUrl : String?
func setupWithImageUrl (imageUrl: String) {
self.contentMode = .scaleAspectFill
self.clipsToBounds = true
self.layer.cornerRadius = 14
self.layer.borderWidth = 0
lastImageUrl = imageUrl
if let cachedImage = imageCache[imageUrl] {
self.image = cachedImage
return
}
guard let url = URL(string: imageUrl) else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print(error)
return
}
guard let data = data else {
return
}
guard let image = UIImage(data : data) else {
return
}
imageCache[url.absoluteString] = image
}
if (url.absoluteString != self.lastImageUrl) {
return
}
DispatchQueue.main.async {
self.image = self.image
}
}
}

Why "this way" cannot make sure correct image is loaded in the image view. I read your code I think your code is fine for collectionViewController cellForItemAt

Related

How to hide tableview until images are completely loaded iOS Swift?

My images take a second to load before they appear, which looks bad. On apps such as instagram, the tableview is hidden until the tableview is loaded... how do they do this? I have a loader that I want to display but don't know when to stop it and show tableview and detect the images have first finished loading? Where do I put stopTimer() ?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MainTableViewCell",
for: indexPath) as! MainTableViewCell
let payment = self.payments[indexPath.row]
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
if let profileImageUrl = payment.picture {
cell.profilePicture.loadImageUsingCacheWithUrlString(profileImageUrl)
}
if payment.message == "none" {
cell.detailsLabel.text = "No Message"
} else {
cell.detailsLabel.text = "\"\(payment.message ?? "")\""
}
}
MY CODE TO FETCH IMAGE IN TABLEVIEW:
let imageCache = NSCache<NSString, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
//check cache for image first
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//otherwise fire off a new download
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
//download hit an error so lets return out
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()
}
}
You can simple use SDWebImage with cocoaPods and use it for async image downloader with cache support. Your cell will look like after ad SDWebImage.
import SDWebImage
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MainTableViewCell",
for: indexPath) as! MainTableViewCell
let payment = self.payments[indexPath.row]
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
if let profileImageUrl = payment.picture {
cell.profilePicture.sd_setImage(with: profileImageUrl, placeholderImage: UIImage(named: "yourPlaceholderImage.png"))
}
if payment.message == "none" {
cell.detailsLabel.text = "No Message"
} else {
cell.detailsLabel.text = "\"\(payment.message ?? "")\""
}
}
There is no need to hide tableView for downloading image.

Failling to reorder tableView cell images

For study purposes, I'm creating a app to show a list of some star wars ships. It fetches my json (locally) for the ship objects (it has 4 ships for this example).
It's using a custom cell for the table view.
The table populates without problems, if I already have the images downloaded (in user documents) or not.
My starshipData array is populated by my DataManager class by delegate.
I removed some code to make the class smaller, I can show everything if needed.
Ok, so the problem happens (very rarely) when I press the sorting button.
The way I'm doing it is after recovering or downloading the image, I update the image field in starshipData array.
Here is my sorting method, pretty basic.
#objc private func sortByCost(sender: UIBarButtonItem) {
starshipData.sort { $0.costInCredits < $1.costInCredits }
starshipTableView.reloadData()
}
Here are the implementations of the tableView.
First I use the cellForRowAt method to populate the fast/light data.
// MARK: -> cellForRowAt
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StarshipCell", for: indexPath) as! StarshipCell
let starship = starshipData[indexPath.row]
// update cell properties
cell.starshipNameLabel.text = starship.name
cell.starshipManufacturerLabel.text = starship.manufacturer
cell.starshipCostLabel.text = currencyFormatter(value: starship.costInCredits)
// only populate the image if the array has one (the first time the table is populated,
// the array doesn't have an image, it'll need to download or fetch it in user documents)
if starship.image != nil {
cell.starshipImgView.image = starship.image
}
// adds right arrow indicator on the cell
cell.accessoryType = .disclosureIndicator
return cell
}
Here I use the willDisplay method to download or fetch the images, basically the heavier data.
// MARK: -> willDisplay
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// update cell image
let cell = cell as! StarshipCell
let imageUrl = starshipData[indexPath.row].imageUrl
let starshipName = starshipData[indexPath.row].name
let index = indexPath.row
// if there isn't any image on the cell, proceed to manage the image
if cell.starshipImgView.image == nil {
// only instantiate spinner on imageView position if no images are set
let spinner = UIActivityIndicatorView(style: .medium)
startSpinner(spinner: spinner, cell: cell)
// manage the image
imageManager(starshipName: starshipName, imageUrl: imageUrl, spinner: spinner, cell: cell, index: index) { (image) in
self.addImageToCell(cell: cell, spinner: spinner, image: image)
}
}
}
Here is where I think the problem is as my knowledge in swift and background threads are still in development.
I found out with print logs that the times the cell doesn't show the correct image is because the array does not have the image for that index, so the cell shows the image from the last time the table was populated/loaded.
I wonder if it's because the background threads didn't have enough time to update the starshipArray with the fetched/downloaded image before the user pushing the sort button.
The thing is, if the table was populated correctly the first time, when the sort button is pushed, the starshipData array should already have all images, as you can see in the imageManager method, after the image is unwrappedFromDocuments, I call updateArrayImage to update the image.
Maybe it's the amount of dispatchesQueues being used? Are the completion handler and dispatchQueues used correctly?
private func imageManager(starshipName: String, imageUrl: URL?, spinner: UIActivityIndicatorView, cell: StarshipCell, index: Int, completion: #escaping (UIImage) -> Void) {
// if json has a string on image_url value
if let unwrappedImageUrl = imageUrl {
// open a background thread to prevent ui freeze
DispatchQueue.global().async {
// tries to retrieve the image from documents folder
let imageFromDocuments = self.retrieveImage(imageName: starshipName)
// if image was retrieved from folder, upload it
if let unwrappedImageFromDocuments = imageFromDocuments {
// TO FORCE THE PROBLEM DESCRIBED, PREVENT ONE SHIP TO HAVE IT'S IMAGE UPDATED
// if (starshipName != "Star Destroyer") {
self.updateArrayImage(index: index, image: unwrappedImageFromDocuments)
// }
completion(unwrappedImageFromDocuments)
}
// if image wasn't retrieved or doesn't exists, try to download from the internet
else {
var image: UIImage?
self.downloadManager(imageUrl: unwrappedImageUrl) { data in
// if download was successful
if let unwrappedData = data {
// convert image data to image
image = UIImage(data: unwrappedData)
if let unwrappedImage = image {
self.updateArrayImage(index: index, image: unwrappedImage)
// save images locally on user documents folder so it can be used whenever it's needed
self.storeImage(image: unwrappedImage, imageName: starshipName)
completion(unwrappedImage)
}
}
// if download was not successful
else {
self.addImageNotFound(spinner: spinner, cell: cell)
}
}
}
}
}
// if json has null on image_url value
else {
addImageNotFound(spinner: spinner, cell: cell)
}
}
Here are some of the helper methods I use on imageManager, if necessary.
// MARK: - Helper Methods
private func updateArrayImage(index: Int, image: UIImage) {
// save image in the array so it can be used when cells are sorted
self.starshipData[index].image = image
}
private func downloadManager(imageUrl: URL, completion: #escaping (Data?) -> Void) {
let session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 5
return URLSession(configuration: configuration, delegate: nil, delegateQueue: nil)
}()
var dataTask: URLSessionDataTask?
dataTask?.cancel()
dataTask = session.dataTask(with: imageUrl) { [weak self] data, response, error in
defer {
dataTask = nil
}
if let error = error {
// use error if necessary
DispatchQueue.main.async {
completion(nil)
}
}
else if let response = response as? HTTPURLResponse,
response.statusCode != 200 {
DispatchQueue.main.async {
completion(nil)
}
}
else if let data = data,
let response = response as? HTTPURLResponse,
response.statusCode == 200 { // Ok response
DispatchQueue.main.async {
completion(data)
}
}
}
dataTask?.resume()
}
private func addImageNotFound(spinner: UIActivityIndicatorView, cell: StarshipCell) {
spinner.stopAnimating()
cell.starshipImgView.image = #imageLiteral(resourceName: "ImageNotFound")
}
private func addImageToCell(cell: StarshipCell, spinner: UIActivityIndicatorView, image: UIImage) {
DispatchQueue.main.async {
spinner.stopAnimating()
cell.starshipImgView.image = image
}
}
private func imagePath(imageName: String) -> URL? {
let fileManager = FileManager.default
// path to save the images on documents directory
guard let documentPath = fileManager.urls(for: .documentDirectory,
in: FileManager.SearchPathDomainMask.userDomainMask).first else { return nil }
let appendedDocumentPath = documentPath.appendingPathComponent(imageName)
return appendedDocumentPath
}
private func retrieveImage(imageName: String) -> UIImage? {
if let imagePath = self.imagePath(imageName: imageName),
let imageData = FileManager.default.contents(atPath: imagePath.path),
let image = UIImage(data: imageData) {
return image
}
return nil
}
private func storeImage(image: UIImage, imageName: String) {
if let jpgRepresentation = image.jpegData(compressionQuality: 1) {
if let imagePath = self.imagePath(imageName: imageName) {
do {
try jpgRepresentation.write(to: imagePath,
options: .atomic)
} catch let err {
}
}
}
}
private func startSpinner(spinner: UIActivityIndicatorView, cell: StarshipCell) {
spinner.center = cell.starshipImgView.center
cell.starshipContentView.addSubview(spinner)
spinner.startAnimating()
}
}
To sum all up, here is the unordered table, when you open the app: unordered
The expected result (happens majority of time), after pushing the sort button: ordered
The wrong result (rarely happens), after pushing the sort button: error
I'll gladly add more info if needed, ty!
First, consider move the cell configuration for the UITableViewCell class. something like this:
class StarshipCell {
private var starshipNameLabel = UILabel()
private var starshipImgView = UIImageView()
func configure(with model: Starship) {
starshipNameLabel.text = model.name
starshipImgView.downloadedFrom(link: model.imageUrl)
}
}
Call the configure(with: Starship) method in tableView(_:cellForRowAt:).
The method downloadedFrom(link: ) called inside the configure(with: Starship) is provide by following extension
extension UIImageView {
func downloadedFrom(url: URL, contentMode mode: UIView.ContentMode = .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: UIView.ContentMode = .scaleAspectFit) {
if let link = link {
guard let url = URL(string: link) else { return }
downloadedFrom(url: url, contentMode: mode)
}
}
}

Set a default image in TableViewCell

I have tried several different approaches and nothing has yet to work. I am pulling in album artwork for a recently played tableview for my radio station app. I get blank images when there is no album artwork to pull into the cell. I just want to have my station logo "WhiteLogo.png" as a placeholder whenever there is no album artwork pulled into the tableview cell. Any help in the right direction is much appreciated. Thanks
import UIKit
//----------
//MARK: JSON
//----------
//The Initial Response From The JSON
struct Response: Codable {
var playHistory: Album
}
//The Album Received Which Is An Array Of Song Data
struct Album: Codable {
var song: [SongData]
}
//The SongData From The PlayHistory Album
struct SongData: Codable{
var album: String
var artist: String
var cover: String
var duration: String
var programStartTS: String
var title: String
}
class TableViewController: UITableViewController {
//1. Create An Array To Store The SongData
var songs = [SongData]()
var currentStation: RadioStation!
var downloadTask: URLSessionDownloadTask?
override func viewDidLoad() { super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
//2. Load The JSON From The Main Bundle
guard let urlText = URL (string: "http://streamdb3web.securenetsystems.net/player_status_update/JACKSON1_history.txt")
else { return }
do{
//a. Get The Data From The From The File
let data = try Data(contentsOf: urlText)
//b. Decode The Data To Our Structs
let albumData = try JSONDecoder().decode(Response.self, from: data)
//c. Append The Songs Array With The PlayHistory
albumData.playHistory.song.forEach { songs.append($0) }
//d. Test Some Data
print("""
**The First Album Details**
Album = \(songs[0].album)
Artist = \(songs[0].artist)
Cover = \(songs[0].cover)
Duration = \(songs[0].duration)
Start = \(songs[0].programStartTS)
Title = \(songs[0].title)
""")
//3. Load The Data
DispatchQueue.main.async {
self.tableView.reloadData()
}
}catch{
print(error)
}
}
//-----------------
//MARK: UITableView
//-----------------
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return songs.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//1. Create A Cell
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
//2. Set It's Text
cell.songTitle.text = songs[indexPath.row].title
cell.artistLabel.text = songs[indexPath.row].artist
//3. Get The Image
if let imageURL = URL(string: songs[indexPath.row].cover){
let request = URLSession.shared.dataTask(with: imageURL) { (imageData, response, error) in
if let error = error{
print(error)
}else{
guard let image = imageData else { return }
DispatchQueue.main.async {
cell.songCover.image = UIImage(data: image)
cell.setNeedsLayout()
cell.layoutIfNeeded()
}
}
}
request.resume()
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("""
**Album \(indexPath.row) Selected**
Album = \(songs[indexPath.row].album)
Artist = \(songs[indexPath.row].artist)
Cover = \(songs[indexPath.row].cover)
Duration = \(songs[indexPath.row].duration)
Start = \(songs[indexPath.row].programStartTS)
Title = \(songs[indexPath.row].title)
""")
}
}
Just the right case handling is required.
I would set the placeholder image first and then proceed to download an image from a URL.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//...
/*
Start with placeholder image so it shows until the image download completes.
And if the next `if let imageURL` condition fails, the placeholder image will stay
*/
cell.songCover.image = UIImage(named: "WhiteLogo")
//Continue with your logic, no change really but could be shortened to:
if let imageURL = URL(string: songs[indexPath.row].cover) {
let request = URLSession.shared.dataTask(with: imageURL) { (imageData, response, error) in
guard let imageData = imageData else { return }
DispatchQueue.main.async {
cell.songCover.image = UIImage(data: imageData)
}
}
request.resume()
}
//...
}
However, since the image download logic is async, it will misbehave if the cell is reused before the download completes.
i.e. Image download for the first song starts but you scroll fast enough to reuse the first cell for, lets say, the third song.
Now, when the download completes, the first image could show on the third cell.
If you face this issue then let me know and I shall update my answer.
Set "WhiteLogo.png" on above your code which download image for album or set logo image if album image data is nil like guard let image = imageData else { var image : UIImage = UIImage(named:"WhiteLogo.png")!
cell.songCover.image = UIImageView(image: image) }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//1. Create A Cell
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
//2. Set It's Text
cell.songTitle.text = songs[indexPath.row].title
cell.artistLabel.text = songs[indexPath.row].artist
//set image
var image : UIImage = UIImage(named:"WhiteLogo.png")!
cell.songCover.image = UIImageView(image: image)
//3. Get The Image
if let imageURL = URL(string: songs[indexPath.row].cover){
let request = URLSession.shared.dataTask(with: imageURL) { (imageData, response, error) in
if let error = error{
print(error)
}else{
guard let image = imageData else { return }
DispatchQueue.main.async {
cell.songCover.image = UIImage(data: image)
cell.setNeedsLayout()
cell.layoutIfNeeded()
}
}
}
request.resume()
}
return cell
}
guard let image = imageData else { cell.songCover.image = UIImage(named : "your_image_name"); return }
Please use the Kingfisher library it will download image from url and set placeholder image.Library URL:- https://github.com/onevcat/Kingfisher

Swift Images change to wrong images while scrolling after async image loading to a UITableViewCell

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

Swift update tableview async with S3 download function

I have an async problem - I have a function that loads images from S3, stores them into an array of UIImages (here called Images)
I also have a tableview that loads its cells from firebase fetched data, my question is, how to update the cell image once the async finishes loading ?
I'm also afraid that the queue of images won't match exactly the indexPath.row since some images might load faster than other images.
func download(key:String, myindex:NSIndexPath, myrow:Int) -> NSString {
let path:NSString = NSTemporaryDirectory().stringByAppendingString("image.jpg")
let url:NSURL = NSURL(fileURLWithPath: path as String)
// let downloadingFilePath = downloadingFileURL.path!
let downloadRequest = AWSS3TransferManagerDownloadRequest()
downloadRequest.bucket = "witnesstest/" + rootref.authData.uid
downloadRequest.key = key
downloadRequest.downloadingFileURL = url
switch (downloadRequest.state) {
case .NotStarted, .Paused:
let transferManager = AWSS3TransferManager.defaultS3TransferManager()
transferManager.download(downloadRequest).continueWithBlock({ (task) -> AnyObject! in
if let error = task.error {
if error.domain == AWSS3TransferManagerErrorDomain as String
&& AWSS3TransferManagerErrorType(rawValue: error.code) == AWSS3TransferManagerErrorType.Paused {
print("Download paused.")
} else {
print("download failed: [\(error)]")
}
} else if let exception = task.exception {
print("download failed: [\(exception)]")
} else {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let tempimage:UIImage = UIImage(contentsOfFile: path as String)!
print("dl ok")
self.Images.append(tempimage)
})
}
return nil
})
break
default:
break
}
return path
}
and the cell :
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: MainWitnessTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("RIcell") as! MainWitnessTableViewCell
// populate the cell
let postitem = posts[indexPath.row]
cell.postOwner.text = postitem.author
cell.postContent.text = postitem.content
cell.postDate.text = postitem.createon
let myindex = indexPath
let myrow = indexPath.row
// cell.cellImage.image = Images[indexPath.row] // returns array index out of range
// download(postitem.imagekey, myindex: myindex, myrow: myrow)
return cell
}
You can just set the imageView's image after you download the image and at the same time set postitem's image property (assuming you add one). It's hard for me to understand everything your download method is doing, but I think the gist of what you want is something like:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: MainWitnessTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("RIcell") as! MainWitnessTableViewCell
// populate the cell
let postitem = posts[indexPath.row]
cell.postOwner.text = postitem.author
cell.postContent.text = postitem.content
cell.postDate.text = postitem.createon
//postitem should also have a imageURL property or you should have some way of getting the image url.
if post item.image == nil{
dispatch_queue_t imageFetchQ = dispatch_queue_create("image fetcher", NULL)
dispatch_async(imageFetchQ, ^{
let imageData = NSData(contentsOfURL: postitem.imageURL)
let image = UIImage(data: imageData)
postitem.image = image
dispatch_async(dispatch_get_main_queue(), ^{
//the UITableViewCell may have been dequeued and reused so check if the cell for the indexPath != nil
if tableView.cellForRowAtIndexPath(indexPath) != nil{
cell.imageView.image = image
}
})
})
return cell
}
I would recommend you create a cache of images and the indices they're supposed to be associated with, and then instead of loading all the images in your function, you would create the table view, and then for that specific cell ask for the image from your server or from the cache if it's already there, rather then trying to download them all async at one time

Resources