Why can not I set image?
class ViewController:UIViewController,UITableViewDelegate,UITalbeViewDataSource{
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell",for: indexPath) as! CustomTableViewCell
cell.setCell()
return cell
}
}
class CustomTableViewCell:UITableViewCell{
#IBOutlet weak var artworkImage:UIImageView!
func setCell(){
let urlStr = "http://is3.mzstatic.com/image/thumb/Music20/v4/d4/6c/af/d46caf98-ff6c-1707-135d-58d6ed9ea6a2/source/500x500bb.jpg"
let url = URL(strings: urlStr)
let data = try? Data(contentsOf: url!)
if let imageData = data {
self.artworkImage.image = UIImage(data: imageData)
}
}
}
I want to display the image of the URL destination in TableView but the image is not displayed.
this image is log.
setCell() should be like this
func setCell(){
let urlStr = "http://is3.mzstatic.com/image/thumb/Music20/v4/d4/6c/af/d46caf98-ff6c-1707-135d-58d6ed9ea6a2/source/500x500bb.jpg"
let url = URL(string: urlStr)
artworkImage.image = UIImage(named: "ic_placeholder")
DispatchQueue.global(qos: .background).async {
let data = try? Data(contentsOf: url!)
DispatchQueue.main.async {
if let imageData = data {
self.artworkImage.image = UIImage(data: imageData)
}
}
}
}
Image should be downloaded on background thread otherwise UI will be blocked. After downloading images you need to make changes in Main Thread.
Also better have a placeholder image.
You are trying to set image in background thread, because of that you receive this logs. You need to use main thread while displaying image on imageView like:
if let imageData = data {
DispatchQueue.main.async(execute: {
self.artworkImage.image = UIImage(data: imageData)
})
}
Try this Lib for images particularly for ImageView on cell :-
https://github.com/Haneke/HanekeSwift
let URLString = self.items[indexPath.row]
let URL = NSURL(string:URLString)!
cell.imageView.hnk_setImageFromURL(URL)
cell:
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var artworkImage:UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
func setCell(){
let urlStr = "http://is3.mzstatic.com/image/thumb/Music20/v4/d4/6c/af/d46caf98-ff6c-1707-135d-58d6ed9ea6a2/source/500x500bb.jpg"
let url = URL(string: urlStr)
let data = try? Data(contentsOf: url!)
DispatchQueue.main.async {
if let imageData = data {
self.artworkImage.image = UIImage(data: imageData)
} }
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Also do not forget. Set up App Transport Security Settings.
good luck
Related
I've programmatically setup a collection view that should display an image from an API onto a cell. However, once the cells are displayed and data is called the cells remain empty.
Cells returning an empty image view
Current setup
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(ImageCell.self, forCellWithReuseIdentifier: imageCellId)
loadImageData(numberOfItems: numberOfItems)
}
func loadImageData(numberOfItems: Int) {
client.getImageData(items: numberOfItems, completion: { (error,data) in
if error != nil {
print("Error parsing image data")
} else {
self.per_page = data.perPage
self.total_results = data.totalResults
self.images = data.photos
for image in self.images {
self.userNameArray.append(image.photographer)
self.urlArray.append(image.url)
}
self.imageLinkArray = self.urlArray
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
})
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: imageCellId, for: indexPath) as! ImageCell
let imageString = String(imageLinkArray[indexPath.row])
let url = URL(string: imageString)
let data = try? Data(contentsOf: url!)
if let imageData = data {
let imageFromDatabase = UIImage(data: imageData)
cell.imageView.image = imageFromDatabase
}
return cell
}
Tried:
Made sure the URLs are coming back using a print statement for the url constant in cellForItemAt.
I've also tested out the cell layout by using a placeholder image.
Called collectionView.reloadData() in viewDidAppear().
Collection View Cells not appearing
Images not displayed in collection view cell
You should use Kingfisher as an image caching library
https://github.com/onevcat/Kingfisher
First, add the pod to your project:
pod 'Kingfisher'
Replace this:
let url = URL(string: imageString)
let data = try? Data(contentsOf: url!)
if let imageData = data {
let imageFromDatabase = UIImage(data: imageData)
cell.imageView.image = imageFromDatabase
}
with this
let url = URL(string: imageString)
imageView.kf.setImage(with: url)
I have tableview with label, imageView (for image, gif & video thumbnail). I am sure that doing something wrong and I can't handle its completion handler due to which the app is hanged and gets stuck for a long time.
My model is like,
struct PostiisCollection {
var id :String?
var userID: String?
var leadDetails : NSDictionary?
var company: NSDictionary?
var content: String?
init(Doc: DocumentSnapshot) {
self.id = Doc.documentID
self.userID = Doc.get("userID") as? String ?? ""
self.leadDetails = Doc.get("postiiDetails") as? NSDictionary
self.company = Doc.get("company") as? NSDictionary
self.content = Doc.get("content") as? String ?? ""
}
}
I wrote in my view controller for fetch this,
var postiisCollectionDetails = [PostiisCollection]()
override func viewDidLoad() {
super.viewDidLoad()
let docRef = Firestore.firestore().collection("PostiisCollection").whereField("accessType", isEqualTo: "all_access")
docRef.getDocuments { (querysnapshot, error) in
if let doc = querysnapshot?.documents, !doc.isEmpty {
print("Document is present.")
for document in querysnapshot!.documents {
_ = document.documentID
if let compCode = document.get("company") as? NSDictionary {
do {
let jsonData = try JSONSerialization.data(withJSONObject: compCode)
let companyPost: Company = try! JSONDecoder().decode(Company.self, from: jsonData)
if companyPost.companyCode == AuthService.instance.companyId ?? ""{
print(AuthService.instance.companyId ?? "")
if (document.get("postiiDetails") as? NSDictionary) != nil {
let commentItem = PostiisCollection(Doc: document)
self.postiisCollectionDetails.append(commentItem)
}
}
} catch {
print(error.localizedDescription)
}
DispatchQueue.main.async {
self.tableView.isHidden = false
self.tableView.reloadData()
}
}
}
}
}
}
I need to check for the index path with image view is either image or gif or video with different parameters, I tried with tableview delegate and datasource method by,
extension AllAccessPostiiVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postiisCollectionDetails.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AllAccessCell", for: indexPath)
let label1 = cell.viewWithTag(1) as? UILabel
let imagePointer = cell.viewWithTag(3) as? UIImageView
let getGif = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "video") as? NSArray
label1?.text = "\(arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "title"))"
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
print(arrGif[0])
let gifURL : String = "\(arrGif[0])"
let imageURL = UIImage.gifImageWithURL(gifURL)
imagePointer?.image = imageURL
playButton?.isHidden = true
}
if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
print(arrPhoto[0])
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
do {
let data = try Data(contentsOf: url!)
let image = UIImage(data: data as Data)
DispatchQueue.main.async {
imagePointer?.image = image
playButton?.isHidden = true
}
} catch {
print(error)
}
})
}
if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL = URL(string: arrVideo[0])
let asset = AVAsset(url:videoURL!)
if let videoThumbnail = asset.videoThumbnail{
SVProgressHUD.dismiss()
imagePointer?.image = videoThumbnail
playButton?.isHidden = false
}
}
}
}
If I run, the app hangs in this page and data load time is getting more, some cases the preview image is wrongly displayed and not able to handle its completion
As others have mentioned in the comments, you are better of not performing the background loading in cellFroRowAtIndexPath.
Instead, it's better practice to add a new method fetchData(), where you perform all the server interaction.
So for example:
// Add instance variables for fast access to data
private var images = [UIImage]()
private var thumbnails = [UIImage]()
private func fetchData(completion: ()->()) {
// Load storage URLs
var storageURLs = ...
// Load data from firebase
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
// Parse data and store resulting image in image array
...
// Call completion handler to indicate that loading has finished
completion()
})
}
Now you can call fetchData() whenever you would like to refresh data and call tableview.reloadData() within the completion handler. That of course must be done on the main thread.
This approach simplifies your cellForRowAtIndexPath method.
There you can just say:
imagePointer?.image = ...Correct image from image array...
Without any background loading.
I suggest using below lightweight extension for image downloading from URL
using NSCache
extension UIImageView {
func downloadImage(urlString: String, success: ((_ image: UIImage?) -> Void)? = nil, failure: ((String) -> Void)? = nil) {
let imageCache = NSCache<NSString, UIImage>()
DispatchQueue.main.async {[weak self] in
self?.image = nil
}
if let image = imageCache.object(forKey: urlString as NSString) {
DispatchQueue.main.async {[weak self] in
self?.image = image
}
success?(image)
} else {
guard let url = URL(string: urlString) else {
print("failed to create url")
return
}
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
// response received, now switch back to main queue
DispatchQueue.main.async {[weak self] in
if let error = error {
failure?(error.localizedDescription)
}
else if let data = data, let image = UIImage(data: data) {
imageCache.setObject(image, forKey: url.absoluteString as NSString)
self?.image = image
success?(image)
} else {
failure?("Image not available")
}
}
}
task.resume()
}
}
}
Usage:
let path = "https://i.stack.imgur.com/o5YNI.jpg"
let imageView = UIImageView() // your imageView, which will download image
imageView.downloadImage(urlString: path)
No need to put imageView.downloadImage(urlString: path) in mainQueue, its handled in extension
In your case:
You can implement following logic in cellForRowAt method
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let urlString : String = "\(arrGif[0])"
let image = UIImage.gifImageWithURL(urlString)
imagePointer?.image = image
playButton?.isHidden = true
}
else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let urlString = Storage.storage().reference(forURL: arrPhoto[0])
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = true
}
elseif getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let urlString = arrVideo[0]
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = false
}
If you have one imageView to reload in tableView for photo, video and gif. then use one image array to store it prior before reloading. So that your main issue of hang or stuck will be resolved. Here the main issue is each time in table view cell collection data is being called and checked while scrolling.
Now I suggest to get all photo, gifs and video (thumbnail) as one single array prior to table view reload try this,
var cacheImages = [UIImage]()
private func fetchData(completionBlock: () -> ()) {
for (index, _) in postiisCollectionDetails.enumerated() {
let getGif = postiisCollectionDetails[index].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = postiisCollectionDetails[index].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = postiisCollectionDetails[index].leadDetails?.value(forKey: "video") as? NSArray
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let gifURL : String = "\(arrGif[0])"
self.randomList.append(gifURL)
/////---------------------------
let imageURL = UIImage.gifImageWithURL(gifURL)
self.cacheImages.append(imageURL!)
//////=================
}
else if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL: String = "\(arrVideo[0])"
let videoUrl = URL(string: arrVideo[0])
let asset = AVAsset(url:videoUrl!)
if let videoThumbnail = asset.videoThumbnail{
////--------------
self.cacheImages.append(videoThumbnail)
//-----------
}
self.randomList.append(videoURL)
}else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let photoURL : String = "\(arrPhoto[0])"
/////---------------------------
let url = URL(string: photoURL)
let data = try? Data(contentsOf: url!)
if let imageData = data {
let image = UIImage(data: imageData)
if image != nil {
self.cacheImages.append(image!)
}
else {
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
}
}
//////=================
}
else {
//-----------------
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
//--------------------
}
}
completionBlock()
}
After getting all UIImage as array where loop is being called. Now you call this function inside your viewDidLoad. So after all values in images fetched then try to call tableView like this,
override func viewDidLoad() {
self.fetchData {
DispatchQueue.main.async
self.tableView.reloadData()
}
}
}
Now atlast, you may use SDWebImage or any other background image class or download method to call those in tableView cellforRow method,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// your cell idetifier & other stuffs
if getVideo != nil {
imagePointer?.image = cacheImages[indexPath.row]
playButton?.isHidden = false
}else {
imagePointer?.image = cacheImages[indexPath.row]
// or get photo with string via SdWebImage
// imagePointer?.sd_setImage(with: URL(string: photoURL), placeholderImage: UIImage(named: "edit-user-80"))
playButton?.isHidden = true
}
return cell
}
You're handling data in a totally wrong manner. Data(contentsOf: url!) - This is wrong. You should chache the images and should download it to directory. When you convert something into data it takes place into the memory(ram) and it is not good idea when playing with large files. You should use SDWebImage kind of library to set images to imageview.
Second thing if let videoThumbnail = asset.videoThumbnail - This is also wrong. Why you're creating assets and then getting thumbnail from it? You should have separate URL for the thumbnail image for your all videos in the response of the API and then again you can use SDWebImage to load that thumbnail.
You can use SDWebImage for gif as well.
Alternative of SDWebImage is Kingfisher. Just go through both libraries and use whatever suitable for you.
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 am learning swift and I finally found how to load images asynchronously . What I am trying to do now is pass that code to my TableView however I am a bit stumped on how to do that given how my code files are structured . First of here is the code that shows a single image.
login.swift:
let imgURL: NSURL = NSURL(string: "my-url")!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
self.Profile_Image.image = UIImage(data: data!)
})
});
task.resume()
That code above correctly renders an image now I want to pass that code and insert it into a TableView: I have 2 files 1 for the TableViewCell and the other a controller with a TableView .
TableViewCell.swift:
class TableViewCell: UITableViewCell {
#IBOutlet weak var fullname: UIButton!
#IBOutlet weak var profile_image: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
Controller.swift:
class Controller: UIViewController,UITableViewDataSource {
var FullName = [String]()
var profile_image_string = [String]()
#IBOutlet weak var TableSource: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
TableSource.dataSource = self
getJsonData()
}
func getJsonData() {
URLSession.shared.dataTask(with:request, completionHandler: {(data, response, error) in
if error != nil {
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:Any]
if let Streams = parsedData["JsonData"] as? [AnyObject] {
for Stream in Streams {
if let fullname = Stream["fullname"] as? String {
self.FullName.append(fullname)
}
if let profile_picture = Stream["profile_image"] as? String {
self.profile_image_string.append(profile_picture)
}
}
DispatchQueue.main.async {
self.TableSource.reloadData()
}
}
} catch let error as NSError {
print(error)
}
}
}).resume()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TVC", for: indexPath) as! TableViewCell
cell.fullname.setTitle(FullName[indexPath.row],for: UIControlState.normal)
cell.profile_image.tag = indexPath.row
return cell
}
}
Ok so at the top of my controller class I have the variable var profile_image_string which basically saves all the different URL's that are coming from the Json then I append it by using this code
if let profile_picture = Stream["profile_image"] as? String {
self.profile_image_string.append(profile_picture)
}
now I only have access to the ImageView property inside the TableView code, how can I display my image ? if you can see the code inside the TableView(UITableViewCell) I access the imageView by doing this
cell.profile_image. ///
any suggestions would be great ...
You can define your cellForRowAt method like below:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TVC", for: indexPath) as! TableViewCell
cell.fullname.setTitle(FullName[indexPath.row],for: UIControlState.normal)
cell.profile_image.tag = indexPath.row
/* Set your cell image here*/
let strCellImageURL = yourArray.objectAtIndex(indexPath.row)
let imgURL: NSURL = NSURL(string: strCellImageURL)!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
cell.Profile_Image.image = UIImage(data: data!)
})
});
return cell
}
I think if something good is already available in Native so why should we go with Third Party Classes.
If you need any clarification then you can comment me below.
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))