I need to load 100 images in cells. If I use this method in tableView cellForRowAt:
cell.cardImageView.image = UIImage(named: "\(indexPath.row + 1).jpg")
and start scrolling fast my tableView freezes.
I use this method to load the image data in background that fix freezes:
func loadImageAsync(imageName: String, completion: #escaping (UIImage) -> ()) {
DispatchQueue.global(qos: .userInteractive).async {
guard let image = UIImage(named: imageName) else {return}
DispatchQueue.main.async {
completion(image)
}
}
}
in tableView cellForRowAt call this:
loadImageAsync(imageName: "\(indexPath.row + 1).jpg") { (image) in
cell.cardImageView.image = image
}
But I have one bug may arise in this approach, such that while scrolling fast I may see old images for a while. How to fix this bug?
Your cells are being reused.
When cell goes out of screen it gets to internal UITableViews reuse pool, and when you dequeueReusableCell(withIdentifier:for:) in tableView(_:cellForRowAt:) you get this cell again (see, "reusable" in name). It is important to understand UITableViewCell's life cycle. UITableView does not hold 100 living UITableViewCells for 100 rows, that would kill performance and leave apps without memory pretty soon.
Why do you see old images in your cells?
Again, cells are being reused, they keep their old state after reuse, you'll need to reset the image, they won't reset it by themselves. You can do that when you configure a new cell or detect when the cell is about to be reused.
As simple as:
cell.cardImageView.image = nil // reset image
loadImageAsync(imageName: "\(indexPath.row + 1).jpg") { (image) in
cell.cardImageView.image = image
}
The other way is detecting reuse and resetting. In your cell subclass:
override func prepareForReuse() {
super.prepareForReuse()
self.cardImageView.image = nil // reset
}
Why do you see wrong images in your cells? By the time completion closure sets image into cardImageView, UITableViewCell has been reused (maybe, even, more than once). To prevent this you could test if you're setting image in the same cell, for example, store image name with your cell, and then:
// naive approach
let imageName = "\(indexPath.row + 1).jpg"
cell.imageName = imageName
loadImageAsync(imageName: imageName) { (image) in
guard cell.imageName == imageName else { return }
cell.cardImageView.image = image
}
There is a lot of stuff to take care of when designing lists, I won't be going into much detail here. I'd suggest to try the above approach and investigate the web on how to handle performance issues with lists.
in your cell class you need to declare
override func prepareForReuse() {
super.prepareForReuse()
}
to prepare the cell for the reuse
I'm trying to do something very basic, but the fix proposed in other similar questions does not seem to be working. I have an image cache, and a tableView. I want to display the image from the cache if it exists, otherwise there should be nothing. For some reason the tableView is still displaying a reused cell with the wrong image, even when I set the image view to nil. Below is my code:
let cell = tableView.dequeueReusableCell(withIdentifier: "searchCell", for: indexPath) as! SearchResultsTableViewCell
cell.profilePhoto?.image = nil
cell.profilePhoto?.backgroundColor = UIColor.gray
if let userID = myObject.posterId, let profileImage = self.imageCache.object(forKey: userID as AnyObject) {
cell.profilePhoto?.image = profileImage
} else {
if let userId = myObject.posterId {
downloadImage.beginImageDownload() {
(imageOptional) in
if let image = imageOptional {
cell.profilePhoto?.image = image
self.imageCache.setObject(image, forKey: userId as AnyObject)
}
}
}
}
What am I doing wrong? I can't for the life of me figure out why the image is not being set to nil, even though I do that as the first step!
The problem is downloadImage.beginImageDownload closures are holding references to the uitableview cells.
When you complete the image download, you set cell.profilePhoto?.image property, even if the tableView recycles reusable cells to display a different row.
Assign your cell's tag to the indexPath.row and test if cell is still relevant for assigning downloaded image:
/* right after cell dequeue */
cell.tag = indexPath.row
then
/* download finished here */
if cell.tag == indexPath.row {
/* yeah, I want to rock this cell with my downloaded image! */
cell.profilePhoto?.image = downloadedImage
}
Be aware: this will only work in tableview with one section.
P.S. You can place the clean up of your cells in prepareForReuse method inside the SearchResultsTableViewCell to tidy things a little.
You appear to be setting your image to nil but have you considered the downloads which might be in flight when you reuse that cell? It looks like you could be updating a cell's image when the download for some previous index path finishes.
I am programmatically creating cells and adding a delete button to each one of them. The problem is that I'd like to toggle their .hidden state. The idea is to have an edit button that toggles all of the button's state at the same time. Maybe I am going about this the wrong way?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("verticalCell", forIndexPath: indexPath) as! RACollectionViewCell
let slide = panelSlides[indexPath.row]
cell.slideData = slide
cell.slideImageView.setImageWithUrl(NSURL(string: IMAGE_URL + slide.imageName + ".jpg")!)
cell.setNeedsLayout()
let image = UIImage(named: "ic_close") as UIImage?
var deleteButton = UIButton(type: UIButtonType.Custom) as UIButton
deleteButton.frame = CGRectMake(-25, -25, 100, 100)
deleteButton.setImage(image, forState: .Normal)
deleteButton.addTarget(self,action:#selector(deleteCell), forControlEvents:.TouchUpInside)
deleteButton.hidden = editOn
cell.addSubview(deleteButton)
return cell
}
#IBAction func EditButtonTap(sender: AnyObject) {
editOn = !editOn
sidePanelCollectionView.reloadData()
}
I think what you want to do is iterate over all of your data by index and then call cellForItemAtIndexPath: on your UICollectionView for each index. Then you can take that existing cell, cast it to your specific type as? RACollectionViewCell an then set the button hidden values this way.
Example (apologies i'm not in xcode to verify this precisely right now but this is the gist):
for (index, data) in myDataArray.enumerated() {
let cell = collectionView.cellForRowAtIndexPath(NSIndexPath(row: index, section: 0)) as? RACollectionViewCell
cell?.deleteButton.hidden = false
}
You probably also need some sort of isEditing Boolean variable in your view controller that keeps track of the fact that you are in an editing state so that as you scroll, newly configured cells continue to display with/without the button. You are going to need your existing code above as well to make sure it continues to work as scrolling occurs. Instead of creating a new delete button every time, you should put the button in your storyboard and set up a reference too and then you can just use something like cell.deleteButton.hidden = !isEditing
I am making a forum app similar to stack overflow. There is a UITableView displaying the answers to a question, and each cell (which contains an answer) also contains a 'correct answer' tick button. Only one answer can be selected as the correct answer at any given time. The correct answer is marked with a green tick, whilst the others are marked with a grey tick (I have two identical images but with different colors). I am trying to set it so when the user clicks on a tick button that is grey, the currently green button turns grey, and the grey button that was tapped turns green.
#IBOutlet weak var answeredTick: UIButton!
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
cell.answeredTick.tag = indexPath.row
}
#IBAction func answerTickPressed(sender: AnyObject) {
guard let answeredTick = sender as? UIButton else { return }
//indexOfOfficialAnswerId is the currently registered official ID
if let buttonToMakeGrey = answeredTick.viewWithTag(self.indexOfOfficialAnswerId) as? UIButton {
print("success!")
buttonToMakeGrey.setImage(UIImage(named: "greyTick.png"), forState: UIControlState.Normal)
} else {
print("no success")
}
}
However it always prints "no success". The value of indexOfOfficialAnswerId is correct during the if statement. I am able to make answerTicks that are grey turn green, but i cant make the green one grey. Why is this?
UPDATE: It appears that i can make the green tick grey if i click on the green tick, but not if i click on any of the grey ticks
You can find cell with green tick as so:
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.indexOfOfficialAnswerId inSection:0];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
And then use your:
cell.answeredTick.setImage(UIImage(named: "greyTick.png"), forState: UIControlState.Normal)
Sorry for Objective-C code
Try reloading that cell after setting that button image or changing its state as selected. I guess that might be the problem .
I have a tableview that I created with code (without storyboard):
class MSContentVerticalList: MSContent,UITableViewDelegate,UITableViewDataSource {
var tblView:UITableView!
var dataSource:[MSC_VCItem]=[]
init(Frame: CGRect,DataSource:[MSC_VCItem]) {
super.init(frame: Frame)
self.dataSource = DataSource
tblView = UITableView(frame: Frame, style: .Plain)
tblView.delegate = self
tblView.dataSource = self
self.addSubview(tblView)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .Subtitle, reuseIdentifier: nil)
let record = dataSource[indexPath.row]
cell.textLabel!.text = record.Title
cell.imageView!.downloadFrom(link: record.Icon, contentMode: UIViewContentMode.ScaleAspectFit)
cell.imageView!.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
print(cell.imageView!.frame)
cell.detailTextLabel!.text = record.SubTitle
return cell
}
}
and in other class I have an extension method for download images Async:
extension UIImageView
{
func downloadFrom(link link:String?, contentMode mode: UIViewContentMode)
{
contentMode = mode
if link == nil
{
self.image = UIImage(named: "default")
return
}
if let url = NSURL(string: link!)
{
print("\nstart download: \(url.lastPathComponent!)")
NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, _, error) -> Void in
guard let data = data where error == nil else {
print("\nerror on download \(error)")
return
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
print("\ndownload completed \(url.lastPathComponent!)")
self.image = UIImage(data: data)
}
}).resume()
}
else
{
self.image = UIImage(named: "default")
}
}
}
I used this function in other places and worked correctly, Based on my logs I understand that images downloaded without problem (when the cell is rendered) and after download of image, The cell UI not updated.
Also I tried to use caching library like Haneke but problem is exist and not change.
Please help me to understand mistakes
Thanks
After setting the image you should call self.layoutSubviews()
edit: corrected from setNeedsLayout to layoutSubviews
The issue is that the .subtitle rendition of UITableViewCell will layout the cell as soon as cellForRowAtIndexPath returns (overriding your attempt to set the frame of the image view). Thus, if you are asynchronously retrieving the image, the cell will be re-laid out as if there was no image to show (because you're not initializing the image view's image property to anything), and when you update the imageView asynchronously later, the cell will have already been laid out in a manner such that you won't be able to see the image you downloaded.
There are a couple of solutions here:
You can have the download update the image to default not only when there is no URL, but also when there is a URL (so you'll first set it to the default image, and later update the image to the one that you downloaded from the network):
extension UIImageView {
func download(from url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFill, placeholder: UIImage? = nil) {
contentMode = mode
image = placeholder
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
print("error on download \(error ?? URLError(.badServerResponse))")
return
}
guard 200 ..< 300 ~= response.statusCode else {
print("statusCode != 2xx; \(response.statusCode)")
return
}
guard let image = UIImage(data: data) else {
print("not valid image")
return
}
DispatchQueue.main.async {
print("download completed \(url.lastPathComponent)")
self.image = image
}
}.resume()
}
}
This ensures that the cell will be laid out for the presence of an image, regardless, and thus the asynchronous updating of the image view will work (sort of: see below).
Rather than using the dynamically laid out .subtitle rendition of UITableViewCell, you can also create your own cell prototype which is laid out appropriately with a fixed size for the image view. That way, if there is no image immediately available, it won't reformat the cell as if there was no image available. This gives you complete control over the formatting of the cell using autolayout.
You can also define your downloadFrom method to take an additional third parameter, a closure that you'll call when the download is done. Then you can do a reloadRowsAtIndexPaths inside that closure. This assumes, though, that you fix this code to cache downloaded images (in a NSCache for example), so that you can check to see if you have a cached image before downloading again.
Having said that, as I alluded to above, there are some problems with this basic pattern:
If you scroll down and then scroll back up, you are going to re-retrieve the image from the network. You really want to cache the previously downloaded images before retrieving them again.
Ideally, your server's response headers are configured properly so that the built in NSURLCache will take care of this for you, but you'd have to test that. Alternatively, you might cache the images yourself in your own NSCache.
If you scroll down quickly to, say, the 100th row, you really don't want the visible cells backlogged behind image requests for the first 99 rows that are no longer visible. You really want to cancel requests for cells that scroll off screen. (Or use dequeueCellForRowAtIndexPath, where you re-use cells, and then you can write code to cancel the previous request.)
As mentioned above, you really want to do dequeueCellForRowAtIndexPath so that you don't have to unnecessarily instantiate UITableViewCell objects. You should be reusing them.
Personally, I might suggest that you (a) use dequeueCellForRowAtIndexPath, and then (b) marry this with one of the well established UIImageViewCell categories such as AlamofireImage, SDWebImage, DFImageManager or Kingfisher. To do the necessary caching and cancelation of prior requests is a non-trivial exercise, and using one of those UIImageView extensions will simplify your life. And if you're determined to do this yourself, you might want to still look at some of the code for those extensions, so you can pick-up ideas on how to do this properly.
--
For example, using AlamofireImage, you can:
Define a custom table view cell subclass:
class CustomCell : UITableViewCell {
#IBOutlet weak var customImageView: UIImageView!
#IBOutlet weak var customTitleLabel: UILabel!
#IBOutlet weak var customSubtitleLabel: UILabel!
}
Add a cell prototype to your table view storyboard, specifying (a) a base class of CustomCell; (b) a storyboard id of CustomCell; (c) add image view and two labels to your cell prototype, hooking up the #IBOutlets to your CustomCell subclass; and (d) add whatever constraints necessary to define the placement/size of the image view and two labels.
You can use autolayout constraints to define dimensions of the image view
Your cellForRowAtIndexPath, can then do something like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let record = dataSource[indexPath.row]
cell.customTitleLabel.text = record.title
cell.customSubtitleLabel.text = record.subtitle
if let url = record.url {
cell.customImageView.af.setImage(withURL: url)
}
return cell
}
With that, you enjoy not only basic asynchronous image updating, but also image caching, prioritization of visible images because we're reusing dequeued cell, it's more efficient, etc. And by using a cell prototype with constraints and your custom table view cell subclass, everything is laid out correctly, saving you from manually adjusting the frame in code.
The process is largely the same regardless of which of these UIImageView extensions you use, but the goal is to get you out of the weeds of writing the extension yourself.
oh my god, the layoutSubviews is not recommended to use directly
the right way to solve the problem is call:
[self setNeedsLayout];
[self layoutIfNeeded];
here, the two way have to call together.
try this, have a good luck.
Create your own cell by subclassing UITableViewCell. The style .Subtitle, which you are using, has no image view, even if the property is available. Only the style UITableViewCellStyleDefault has an image view.
Prefer SDWebImages library here is the link
it will download image async and cache the image also
and very easy to integrate into the project as well