I have a picture and some labels inside my cells. If I have more cells than what can fit on the page, scrolling down then back up loads a different image momentarily then loads the original photo. I have read around StackOverflow to see what would work in my case, but so far I can't find anything since my UITableView is inside a ViewController.
Here is how I load my content into my cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! PostTableViewCell
// Configure the cell...
let post = self.posts[indexPath.row] as! [String: AnyObject]
cell.titleLabel.text = post["title"] as? String
cell.priceLabel.text = post["price"] as? String
if let imageName = post["image"] as? String {
let imageRef = FIRStorage.storage().reference().child("images/\(imageName)")
imageRef.data(withMaxSize: 25 * 1024 * 1024, completion: { (data, error) -> Void in if error == nil {
let image = UIImage(data: data!)
UIView.animate(withDuration: 0.4, animations: {
cell.titleLabel.alpha = 1
cell.postImageView.alpha = 1
cell.priceLabel.alpha = 1
cell.postImageView.image = image
})
} else {
print("Error occured during image download: \(error?.localizedDescription)")
}
})
}
return cell
}
Is there any way I could change tableView.dequeueReusableCell to something different so this doesn't happen?
In your table view cell PostTableViewCell you need to implement the method
override func prepareForReuse() {
super.prepareForReuse()
self.postImageView.image = nil
// Set cell to initial state here, reset or set values
}
The cells are holding on to their old content
I think you run into problems because you update the cell inside the completion block. If the cell scrolls out of view before the completion block is run, it'll be reused for a different row, but you're still setting the image for the previous row.
Try this:
imageRef.data(withMaxSize: 25 * 1024 * 1024, completion: { (data, error) -> Void in if error == nil {
if let cell = self.tableView.cellForRow(at: indexPath) as? PostTableViewCell {
let image = UIImage(data: data!)
UIView.animate(withDuration: 0.4, animations: {
cell.titleLabel.alpha = 1
cell.postImageView.alpha = 1
cell.priceLabel.alpha = 1
cell.postImageView.image = image
}
})
Instead of relying on the cell still being visible, this will try to get it from the table view based on indexPath. If it's not visible any more, cellForRow(at:) will return nil.
I have just started using swift. I am using blocks and NSOperationQueue to download the image in tableViewCell and in the completion handler I am returning the downloaded image. I am trying to update the cell as below.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("itemCell", forIndexPath: indexPath) as! UITableViewCell
var itemImage = cell.viewWithTag(1000) as! UIImageView
var itemName = cell.viewWithTag(1001) as! UILabel
if let item = self.itemArray?[indexPath.row] {
itemImage.image = UIImage(named: "Placeholder.jpg")
getImageForItem(item, withCompletion: { (image) -> () in
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
var imageViewToUpdate = cellToUpdate.viewWithTag(1000) as! UIImageView
imageViewToUpdate.image = image
}
})
itemName.text = item.itemName
}
return cell
}
func getImageForItem(item: item, withCompletion completion:((image: UIImage) -> ())) {
if let image = self.imageCache.objectForKey(item.itemID) as? UIImage {
completion(image: image)
} else {
let request = item.getItemImage(ItemImageSize(rawValue: 2)!, withWidth: 100, shouldFetch: false, block: { (image, tempID) -> Void in
if image != nil {
self.imageCache.setObject(image, forKey: item.itemID)
if item.itemID == tempID {
completion(image: image)
}
}
})
if request != nil {
imageQueue.addOperation(request)
}
}
}
The problem I face is, I am getting the image successfully in the completion block of cellForRowAtIndexPath(), but, I fail to update the cell. For the above code, the downloaded image is applied to all the visible cells in the tableView, but, as I scroll down, I see only the placeholder image. Even I loose the loaded images to placeholder image on scrolling back.
On debugging, I found that
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
var imageViewToUpdate = cellToUpdate.viewWithTag(1000) as! UIImageView
imageViewToUpdate.image = image
}
loop is called for the visible cells only first time. But not called again on scrolling. What am I missing?
I sorted out myself. I added one more argument, indexPath to track it.
getImageForItem(item, indexPath: indexPath, withCompletion: { (image, imageIndexPath) -> () in
if Set<NSIndexPath>(tableView.indexPathsForVisibleRows() as! [NSIndexPath]).contains(imageIndexPath) {
itemImage.image=image
}
})
That gave me the perfect solution
EDITED CODE FROM BELOW ANSWER
WHAT I WANT
The images are correct size and show in the correct positions
WHAT I GET
A bunch of images showing up everywhere randomly when I scroll
I have looked through so many answers and I still haven't found a solution to my problem. It seems quite often it is a different problem for each person, and it doesn't help that most questions here are in Objective-C. I find it difficult converting from one to another.
I will just show my table view functions and explain my code and maybe somebody can identify the problem or help me to identify to solve it myself.
-----------------------------------------------------------------------------------
Explanation
Not all News Items (cells) contain pictures. So as you can see (in the first function below) I loop through the sections and the nested cells and if the news item contains a picture if newslists[i][j] != "None, I display the image. From this point it is familiar code, seen from the tutorial here
In the second function I once again loop through the sections and the cells till I land on one that should contain an image. I then change the height of this cell so the image will fit inside.
That is pretty much it...
any ideas?
-----------------------------------------------------------------------------------
CellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("NewsCell", forIndexPath: indexPath) as! UITableViewCell
if newslists[indexPath.section][indexPath.row].imageURL != "None"{
let urlString = newslists[indexPath.section][indexPath.row].imageURL
let imgURL = NSURL(string: urlString)
cell.imageView?.image = UIImage(named: "first") // placeholder
if let img = imageCache[urlString] {
cell.imageView?.image = img
println("loaded from cache")
}
else {
let request: NSURLRequest = NSURLRequest(URL: imgURL!)
let mainQueue = NSOperationQueue.mainQueue()
NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
if error == nil {
let image = UIImage(data: data)
self.imageCache[urlString] = image
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
println("succesfully downloaded image")
println("size of cache is \(self.imageCache.count)")
}
})
}
else {
println("Error: \(error.localizedDescription)")
}
})
}
}
cell.backgroundColor = UIColor(red: 52/255, green: 138/255, blue: 169/255, alpha: 1.0)
return cell
}
I have one more function that may be causing problems but I doubt it:
HeightForRowAtIndexPath
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if newslists[indexPath.section][indexPath.row].imageURL != "None"{
println("Expanding cell for \(indexPath.section) \(indexPath.row)")
return 190.0
}
return 70.0
}
I'm not sure why you have a double for loop in both method. There are called for every indexPath possible so you can just get you appropriate data with the indexPath section and row.
You can try this code, i don't see any reason for it to not work.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("NewsCell", forIndexPath: indexPath) as! UITableViewCell
cell.imageView?.image = nil
if newslists[indexPath.section][indexPath.row].imageURL != "None"{
let urlString = newslists[indexPath.section][indexPath.row].imageURL
let imgURL = NSURL(string: urlString)
cell.imageView?.image = UIImage(named: "first") // placeholder
if let img = imageCache[urlString] {
cell.imageView?.image = img
println("loaded from cache")
}
else {
...
}
}
return cell
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if newslists[indexPath.section][indexPath.row].imageURL != "None"{
println("Expanding cell for \(i) \(j)")
return 190.0
}
return 70.0
}
All we need to do is use the prepareForReuse() function. As discussed in this medium article, This function is called before cell reuse, letting you cancel current requests and perform a reset.
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
}
My UITableViewCells images are displaying until I scroll back upwards whereby the images would not be displayed until the cell is selected.
The same problem also happens when I switch from another ViewController to the initial ViewController*(which contains the image)*
I have checked that the imgURL of the image is correct.
Libraries used are: AFNetworking for the image
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("FeedCell", forIndexPath: indexPath) as! MyCell
cell.itemImageView.image = nil
self.configureCell(cell, atIndexPath: indexPath)
return cell
}
// AFNetworking download and display image
func uploadIMG(cell:MyCell,imgURL:NSURL,placeholderIMG:String,atIndexPath indexPath: NSIndexPath) {
var imageRequest: NSURLRequest = NSURLRequest(URL: imgURL)
cell.itemImageView!.setImageWithURLRequest(imageRequest, placeholderImage: UIImage(contentsOfFile: "logo.png"), success: { [weak cell] request,response,image in
if (cell != nil) {
cell!.itemImageView.image = image
}}
, failure: nil)
}
// called from cellForRowAtIndexPath, retrieve img url to update image
func configureCell(cell: MyCell, atIndexPath indexPath: NSIndexPath) {
let item = self.items[indexPath.row] as MWFeedItem
var URLofImage: NSURL = NSURL(string: item.link)!
var session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(URLofImage, completionHandler: {(data,response, error) in
let text = NSString(data: data, encoding: NSUTF8StringEncoding)
var home = HTMLDocument(data: data, contentTypeHeader: text as! String)
var div = home.nodesMatchingSelector("img")
var urlString = div[1].firstNodeMatchingSelector("img")
let urlData = (urlString as HTMLElement).firstNodeMatchingSelector("img")
var urlFinal = urlData.attributes["src"]! as! String
if urlFinal != "/images/system/bookmark-shorturl.png" {
// call updateIMG function
self.uploadIMG(cell, imgURL: NSURL(string: "http:www.animenewsnetwork.com" + urlFinal)!, placeholderIMG: "logo.png",atIndexPath: indexPath)
}
})
Image representation of the problem (Initial image working fine)
Second Image (I scrolled downwards and then scrolled upwards, Image not showing)
I select some cells and the images for those cells will then appear
Try after setting image into cell, update that cell in table view by calling method tableView:reloadRowsAtIndexPaths:withRowAnimation. Or write your custom cell with custom image view. And please, do not forgot that image setting code must run in main thread.
The problem was that my Image wasn't set on the main thread. To solve the problem, I simply used the following code below which ensured that my image will be set immediately.
dispatch_async(dispatch_get_main_queue(), {
// do image functions here
)}
Misread the Question, but keeping this in case anyone has a similar problem, but with autolayout.
I believe you are using autolayout. So if the imageView's frame size is using the intrinsic content size, the size of it's image, it'll be CGSizeZero when there is no image. There is no image when the cell is first displayed, because it needs to be downloaded. So then the image is downloaded and gets assigned to imageView.image. This does not automatically invalidate the layout. You'll need to do that so the imageView frame gets recalculated based on the size of the image. The reason it shows up after scrolling away and scrolling back or selecting it is because the image has been downloaded in that time and the cells layout is recalculated when it gets displayed again or selected.
Below is my TestCell and TestViewController
import UIKit
import AFNetworking
class TestCell : UITableViewCell {
static let cellIdentifier = "TestCell"
#IBOutlet var downloadedImageView: UIImageView!
#IBOutlet var rowLabel: UILabel!
#IBOutlet var statusLabel: UILabel!
}
class TestTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView.rowHeight = 100
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30;
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(TestCell.cellIdentifier, forIndexPath: indexPath) as! TestCell
let randomName = "\(Random.firstName().lowercaseString).\(Random.lastName().lowercaseString)"
let randomImageURL = NSURL(string: Random.avatarImageURL(name: randomName))!
cell.rowLabel.text = String(indexPath.row)
cell.statusLabel.text = "Not Downloaded"
var imageRequest: NSURLRequest = NSURLRequest(URL: randomImageURL)
cell.downloadedImageView.setImageWithURLRequest(imageRequest, placeholderImage: UIImage(named: "placeholder.png"),
success: { [weak cell]
(request, response, image) in
if let cell = cell {
cell.downloadedImageView.image = image
cell.rowLabel.text = String(indexPath.row)
cell.statusLabel.text = "Downloaded"
}
},
failure: { [weak cell]
(request, response, error) in
if let cell = cell {
cell.downloadedImageView.image = nil
cell.rowLabel.text = String(indexPath.row)
cell.statusLabel.text = "Failed: \(error.localizedDescription)"
}
})
return cell
}
}
//
// Random.swift
import Foundation
class Random {
static let firstNames = ["Tora", "Shasta", "Camelia", "Gertrudis", "Charita", "Donita", "Debbra", "Shaquana", "Tommy", "Shara", "Ignacia", "Cassondra", "Melynda", "Lisette", "Herman", "Rhoda", "Farah", "Tim", "Tonette", "Johnathon", "Debroah", "Britni", "Charolette", "Kyoko", "Eura", "Nevada", "Lasandra", "Alpha", "Mirella", "Kristel", "Yolande", "Nelle", "Kiley", "Liberty", "Jettie", "Zoe", "Isobel", "Sheryl", "Emerita", "Hildegarde", "Launa", "Tanesha", "Pearlie", "Julianna", "Toi", "Terina", "Collin", "Shamika", "Suzette", "Tad"]
static let lastNames = ["Austen", "Kenton", "Blomker", "Demars", "Bibbs", "Eoff", "Alcantara", "Swade", "Klinefelter", "Riese", "Smades", "Fryson", "Altobelli", "Deleeuw", "Beckner", "Valone", "Tarbox", "Shumate", "Tabone", "Kellam", "Dibiase", "Fasick", "Curington", "Holbrook", "Sulzer", "Bearden", "Siren", "Kennedy", "Dulak", "Segers", "Roark", "Mauck", "Horsman", "Montreuil", "Leyva", "Veltz", "Roldan", "Denlinger", "James", "Oriley", "Cistrunk", "Rhodes", "Mcginness", "Gallop", "Constantine", "Niece", "Sabine", "Vegter", "Sarnicola", "Towler"]
class func int(#min: Int, max: Int) -> Int {
return Int(arc4random_uniform(UInt32(max-min))) + min //???: RTFM on arc4random, might be need (max+1)-min.
}
class func int(#range: Range<Int>) -> Int {
return int(min: range.startIndex, max: range.endIndex)
}
class func selectElement<T>(#array: [T]) -> T {
return array[int(range: 0..<array.count)]
}
class func firstName() -> String {
return Random.selectElement(array: Random.firstNames)
}
class func lastName() -> String {
return Random.selectElement(array: Random.lastNames)
}
class func avatarImageURL(var name: String? = nil) -> String {
if name == nil {
name = "(Random.firstName().lowercaseString).Random.lastName().lowercaseString"
}
let avatarImageSize = Random.int(min: 40, max: 285)
return "http://api.adorable.io/avatars/\(avatarImageSize)/\(name!)#gmail.png"
}
class func imageURL() -> String {
let imageWidth = Random.int(min:120, max:1080)
let imageHeight = Random.int(min:120, max:1080)
return "http://lorempixel.com/g/\(imageWidth)/\(imageHeight)/"
}
}
When you scroll, cell will reload. (you reload to redownload your image) -> it's problem.
Solved:
You create array for save image data after download.
And cell get image from this array, not redownload
Hope this helpful!
I have a list of reddit posts that I want to display the thumbnail of, if it exists. I have it functioning, but it's very buggy. There are 2 main issues:
Images resize on tap
Images shuffle on scroll
This is the code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Post", forIndexPath: indexPath) as UITableViewCell
let post = swarm.posts[indexPath.row]
cell.textLabel!.text = post.title
if(post.thumb? != nil && post.thumb! != "self") {
cell.imageView!.image = UIImage(named: "first.imageset")
var image = self.imageCache[post.thumb!]
if(image == nil) {
FetchAsync(url: post.thumb!) { data in // code is at bottom, this just drys things up
if(data? != nil) {
image = UIImage(data: data!)
self.imageCache[post.thumb!] = image
dispatch_async(dispatch_get_main_queue(), {
if let originalCell = tableView.cellForRowAtIndexPath(indexPath) {
originalCell.imageView?.image = image
originalCell.imageView?.frame = CGRectMake(5,5,35,35)
}
})
}
}
} else {
dispatch_async(dispatch_get_main_queue(), {
if let originalCell = tableView.cellForRowAtIndexPath(indexPath) {
originalCell.imageView?.image = image
originalCell.imageView?.frame = CGRectMake(5,5,35,35)
}
})
}
}
return cell
}
This is the app when it loads up - looks like everything is working:
Then if I tap on an image (even when you scroll) it resizes:
And if you scroll up and down, the pictures get all screwy (look at the middle post - Generics fun):
What am I doing wrong?
** Pictures and Titles are pulled from reddit, not generated by me **
EDIT: FetchAsync class as promised:
class FetchAsync {
var url: String
var callback: (NSData?) -> ()
init(url: String, callback: (NSData?) -> ()) {
self.url = url
self.callback = callback
self.fetch()
}
func fetch() {
var imageRequest: NSURLRequest = NSURLRequest(URL: NSURL(string: self.url)!)
NSURLConnection.sendAsynchronousRequest(imageRequest,
queue: NSOperationQueue.mainQueue(),
completionHandler: { response, data, error in
if(error == nil) {
self.callback(data)
} else {
self.callback(nil)
}
})
callback(nil)
}
}
Unfortunately, this seems to be a limitation of the "Basic" table view cell. What I ended up doing was creating a custom TableViewCell. A relied on a tutorial by Ray Wenderlich that can be found here: http://www.raywenderlich.com/68112/video-tutorial-table-views-custom-cells
It's a bit of a bummer since the code is so trivial, but I guess on the bright side that means it's a 'simple' solution.
My final code:
PostCell.swift (all scaffolded code)
import UIKit
class PostCell: UITableViewCell {
#IBOutlet weak var thumb: UIImageView!
#IBOutlet weak var title: UILabel!
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
}
}
PostsController.swift
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PostCell", forIndexPath: indexPath) as PostCell
let post = swarm.posts[indexPath.row]
cell.title!.text = post.title
if(post.thumb? != nil && post.thumb! != "self") {
cell.thumb!.image = UIImage(named: "first.imageset")
cell.thumb!.contentMode = .ScaleAspectFit
var image = self.imageCache[post.thumb!]
if(image == nil) {
FetchAsync(url: post.thumb!) { data in
if(data? != nil) {
image = UIImage(data: data!)
self.imageCache[post.thumb!] = image
dispatch_async(dispatch_get_main_queue(), {
if let postCell = tableView.cellForRowAtIndexPath(indexPath) as? PostCell {
postCell.thumb!.image = image
}
})
}
}
} else {
dispatch_async(dispatch_get_main_queue(), {
if let postCell = tableView.cellForRowAtIndexPath(indexPath) as? PostCell {
postCell.thumb!.image = image
}
})
}
}
return cell
}
And my measly storyboard:
I'm not sure the best way to do this, but here a couple of solutions:
Use AFNetworking, like everyone else does. It has the idea of a place holder image, async downloading of the replacement image, and smart caching. Install using cocoa pods, make a bridging file with #import "UIImageView+AFNetworking.h"
Create two different types of cells. Before grabbing a cell with dequeReusableCell... in your cellForRowAtIndexPath, check if it's expanded. If expanded, return and populate an expanded cell otherwise return and populated an unexpanded cell. The cell is usually expanded if it is the 'selected' cell.
Your mileage may vary
it is a huge mistake to call tableView.cellForRowAtIndexPath from within UITableViewDataSource's implementation of tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell. Instead when the async fetch of the thumb image is completed, update your model with the image, then request tableView to reloadRows for that specific cell's indexPath. Let your data source determine the correct indexPath. If the cell is offscreen by the time the image download is complete there will be no performance impact. And of course reloadRows on the main thread.