Force UITableView to cache reusable cells - ios

I use a table view with relatively 'heavy' custom table view cells - say there's is around 100 subviews in a single cell (full hierarchy). Despite of that I've been able to reach very smooth scrolling - using standart practices like working only with opaque views, hiding unused subviews (or even removing from the view hierarchy), pre-loading cells height etc.
The problem is that it's still relatively costly to create a single cell instance. Because of how the standard UITableView's cell reusage logic works this problem is only noticeable in specific conditions.
Usually when table view is loaded (and intitial content is displayed) it creates almost all required cells instances - so when you start scrolling it rarely needs to create any more reusable cells. But if for some reason it only displays a few cells at the beginning it would obviously need to create more when scrolling is initiated. In my case this is noticeable if there's a wide table view header or if first cells are big enough. When that happens once you start scrolling there's a noticeable lag for a few moments which is clearly caused by new cell instances being created. Nothing really ugly but still noticeable especially compared to absolutely smooth scrolling after that.
Now the obvious solution for that would be to make cells 'lighter' - like break them into different more specific types so each can contain less subviews. But because of the way my content is organized that would be a very complex solution.
So my question is - is there any good way to 'trick' table view logic to force loading and caching of specific number of reusable cell instances right away - despite of the number of cells actually visible?
One of the options I've been thinking of is to have own explicit cache of cells and pre-populate it with cells when needed (say in -viewDidLoad). The problem I have with this approach is that you have to known exact type of cells you will need beforehand - doesn't work well for me. The improvement would be to build cache when the initial data is loaded (so at least the exact type of content is known) but I've been wondering if there are any simple options.

You can do it by adding your own "secondary cache" layer to your data source.
When you initialize your data source, pre-create as many cells as needed, and put them into an array. When your dequeueReusableCellWithIdentifier fails, go for that array first, and take a cell from there. Once the array is exhausted, start creating new cells (if you create enough cells upfront, you shouldn't need to create new ones on the fly: dequeuing will eventually stop failing when you have enough cells rotating in and out of visibility).

I too had a similar situation wherein I wanted to load the several websites in different cells and loadrequest must be triggered only once. Idea is to avoid duplicate calls when the cell is made visible/invisible by scrolling up or down. say I have 5 cells in my tableview and each cell has a webview which loads the different websites. loadrequest will be called once and cell will be cached. here goes the simplest implementation.
Variable declaration
let weblinks:[String] = ["http://www.google.com",
"http://www.facebook.com",
"http://www.yahoo.com",
"http://www.puthiyathalaimurai.com/",
"http://www.github.com"]
var mycells:[MyTableViewCell] = [MyTableViewCell(),
MyTableViewCell(),
MyTableViewCell(),
MyTableViewCell(),
MyTableViewCell()]
CellForRow. I hope you know what it does,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = String(describing: MyTableViewCell.self)
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as? MyTableViewCell
if cell?.isNewCell == true { //return from cache
return mycells[indexPath.row]
}
cell?.title.text = weblinks[indexPath.row].uppercased()
cell?.web.loadRequest(URLRequest(url: URL(string: weblinks[indexPath.row])!))
cell?.isNewCell = true //mark it for cache
mycells[indexPath.row] = cell!
return cell!
}

Related

Reusing cells with dynamic layout content at runtime

I have a type of UITableViewCell that lets the user add/remove as many UITextViews as they want at run time.
I'm running into issues when trying to reuse/dequeue cells of that type, as sometimes the tableview cells just start overlapping when you scroll up and down. When I dequeue/return the cell, I'm running a setup method (which initiates a teardown method internally first to remove all the previous views), and uses the model to setup/restore all the necessary views and layout constraints.
if let cell = tableView.dequeueReusableCell(withIdentifier: "MultipleContentCell", for: indexPath) as? MultipleChoiceTableViewCell {
cell.setupCellWithModel(model: model)
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
cell.delegate = self
return cell
}
I can't really figure out why that cells sometimes overlap in the tableview, but I'm guessing it has to do with the layout being recreated on the fly. I'm considering not reusing these types of cells and just storing them in a list.
My question is: are reusable cells always suppose to have the same general UIView layout, and only the content changes? Am I not supposed to use reuse these types of cells? Or has someone experienced this before?
Thanks
The UITextView are created each time you dequeue cell and never delete. To repair that use function prepareForReuse(). You have to define, what your cell should do before dequeue in MultipleChoiceTableViewCell. For example:
override func prepareForReuse() {
super.prepareForReuse()
for view in speciesName.subviews {
if view is UITableView {
view.removeFromSuperview()
}
}
}
I added similar question few days ago:
Cells in UITableView overlapping. Many cells in one place
If you have some question, I can try to help you more tomorrow.
Cheers!
In general, yes. You want the physical layout of your cells to be static, and only vary the contents when you recycle them. If you add views to your cells in cellForRow(at:) then the burden is on you to manage the extra fields to avoid duplicate views.
Your case where you add a variable number of views to a table view cell based on user interaction is an odd case where you might need to add and remove cells on the fly.
One way to handle this would be to put all of your text fields in a container view, add an outlet to that container view, and then simply use code like this in your prepareForReuse or cellForRowAt function:
containerView.subviews.forEach { $0.removeFromSuperview() }

Improve perfomance when scrolling UITableView with custom cells

I have a UITableView which cells are custom, and cell has a view with chart (Charts library). Data for chart is pretty big (it stored as a property of Controller), it appears during few seconds.
Also I do some Chart's View setup (only little UI changes) in setSelected method in custom TableViewCell class
When scrolling table view there some lags appear. I suppose it happens because of heavy content I want to display.
During solving this problem I've thought about four solutions:
Load all cells in ViewDidAppear, save them to array, in CellForRowAt method show cells from this array.
Put table view into scroll view, make content's height pretty big to fit all cells in table view.
Make chart loads only after cell's loading and its content view appearing. Show activity indicator while chart's loading is on.
Put chart's loading in background thread.
First two approaches seem to me a not so good in terms of memory managment. But I am not sure, maybe there is common solution which I have not known.
Some code example from project:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let graphCell = tableView.dequeueReusableCell(withIdentifier: Cells.graphViewCell) as! GraphViewCell
graphCell.lineChartView.data = track.chartDataForHeight()
return graphCell
}
Here's track is variable where I store data for chart.
If you have a very complex cell and not too many of them and you do not want to pay to create it every time, then its correct not to use dequeResuableCell and just lazily create the cells and cache them. You can mitigate the memory problem by responding to didReceiveMemoryWarning and dumping the off screen cells then lazily recreating them. UICollectionView now has prefetching in ios 10. You can also try using this instead of UITableView.

Should I use dequeReusableCellWithIdentifier?

I know this question is dumb. But suddenly got stuck up with this question.
When I use dequeReusableCellWithIdentifier, the cells are reused. To be more specific, first 'n' set of cells is reused - along with their references
As the same references are reused, I can't actually store local variables to the cells. I actually need to assign them everytime in cellForRowAtIndexPath
Assume I'm using a custom complex UITableviewcell. (I know we should reduce complexity. But still...)
Some views are to be added to the cell.
Best example i could think of is an image slider.
So the number of images in the image slider differs based on the datasource. So i need to add the views in cellForRowAtIndexPath. Can't avoid it.
Since the same reference of cells are reused, I need to add these views everytime cellForRowAtIndexPath is called. I think that is a bit of heavy load.
I thought of using drawRect method, but it does not work with UITableViewAutomaticDimension
If I don't use dequeReusableCell, I will have individual referenced cells, but it will cost the memory and performance.
So what is the best solution?
Can I use dequeReusableCell and still need not rewrite its content?
Edit 1:
By mentioning dequeReusableCell, I did mention dequeueReusableCellWithIdentifier - forIndexPath. I'm sorry for the confusion.
Edit 2:
I feel, I'm not so clear in my question.
Lets consider I have a array in my viewcontroller.
I'm configuring my cell in cellForRowAtIndexPath with dequeueReusableCellWithIdentifier - forIndexPath
So what happens is, everytime i scroll, when the invisible rows become visible, cellForRowAtIndexPath is called.
Lets say I have a image slider with n imageviews. For each cell, this 'n' differs. So I'm forced to draw the cell's view based on its dataSource.
So everytime the tableView calls cellForRowAtIndexPath, the cell is configured again and again.
I want to avoid this to improve performance.
what I do in this case is the following:
I always use dequeReusableCell:, for reasons you already said
I write a cleanCell method in the custom UITableViewCell class in order to clean everything has been already set on the cell (just to make it ready for reuse)
then in the cellForRowAtIndexPath: I configure my cell as desired
That's what I would do: I would move the logic of adding those heavy views inside the cell's class itself. So that in cellForRowAtIndexPath I would just set an object that has to be displayed inside the cell, and cell would create all the necessary views on its own (it's a view layer, after all). So inside cellForRowAtIndexPath it would look something like this:
cell = [tableView dequeueReusableCellWithIdentifier...forIndexPath...];
cell.modelObject = myModelObject;
and that's it.
Then inside the cell class I would implement optimizations for switching between different model objects. Like, you could defer releasing the heavy views, and reuse them, when the cell is reused.
To be honest, I don't get your example with the slider, but let's suppose you have a star rating with 5 stars, which can be visible or not. There are various way to do it, but let's assume you're just adding / removing a UIImageView for each star. The way you do it now, you would have an empty cell and create / add views in cellForRow. What I'm suggesting is to have 5 UIImageViews as part of your cell, and inside the cell set their visibility based on the modelObject.rating. Something like that. I hope, it helps to illustrate my point :)
UPDATE: when your cell can have an arbitary number of images inside of it, I would probably create them inside the cell's class. So, for instance, for the first model object we need 3 image views. So we create them. But then we don't release them in prepareForReuse, we wait for the next model object to come. And if it has, say, 1 image, we release 2 image views (or we don't, so that we didn't have to recreate them later, it depends on what is more critical: performance, or memory usage), and if it needs 5, we create two more. And if it needs 3, we're all set already.
In your case you should use dequeueReusableCellWithIdentifier(_ identifier: String, forIndexPath indexPath: NSIndexPath) method. This method automatically decides whether the cell needs to be created or dequeued.
I need to add these views everytime cellForRowAtIndexPath is called. I think that is a bit of heavy load.
You don't need to add those views every time cellForRowAtIndexPath gets called. Those views should be already added as a cell is instantiated. In cellForRowAtIndexPath you only assign images and other values to those views.
In case if you use storyboards and prototype cells your code should look like this:
var items = [UIImage]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ImageCell", forIndexPath: indexPath)
let item = items[indexPath.row]
let recipient = item.recipients.first
// I have given my UIImageView tag = 101 to easily access it from the view controller.
// This allows me to avoid defining a custom UITableViewCell subclass for simple cells.
if let imageView = cell.viewWithTag(101) as? UIImageView {
imageView.image = item
}
return cell
}
1)I think you need to use "lazy load" of cells.
http://www.theappguruz.com/blog/ios-lazy-loading-images (for example)
2) You can create a property in your viewController, and create an NSMutableDictionary property, which will store the some data by the some key,
if your model have things like "ID" of the cells or you can use indexPath.row for it, then you can load it only one time, and next time when -cellForRow will be called, you can retrieve data from your dictionary by the
[self.dataDictionary objectForKey:#"cellID"];
And with this you, solve your repeating load.
In -cellForRow you can set the check,like.
if ([self.dataDictionary objectForKey:#"cellID"]) {
cell.cellImage = [self.dataDictionary objectForKey:#"cellID"];
} else {
UIImage *img = [uiimage load_image];
cell.cellImage = img;
[self.dataDictionary setObject:img forKey:#"cellID"];
}
And so on. Its like example, the dataType's you can choose by yourself, it's example.
Thanks for everyone helping me out. I think the better answer was from #Eiko.
When you use reusable cells, it must be completely reusable. If you are going to change its views based on the data, you either have to use a new identifier or avoid using reusable cells.
But avoiding reusable cells, will take a load. So you should either use different identifiers for similar cells or as #FreeNickName told - you can use an intermediate configuration and alter it on your need.

Multiple cells got affected after calling cellForRowAtIndexPath

I have a UITableView, and I want to change text color of the selected row. But I see every other 15 row got affected along with the one I clicked. This is tested under the master-detail sample project.
if let indexPath = self.tableView.indexPathForSelectedRow(){
self.tableView.cellForRowAtIndexPath(indexPath)?.textLabel?.textColor = UIColor.redColor()
}
I checked cellForRowAtIndexPath value in debug session, it seems returning only one object, how come other cells got affected too?
Cells are reused - almost certainly you are not resetting your cells to a base state in prepareForReuse and configuring them correctly on each call to cellForRowAtIndexPath.
When you look at a table view that can display some number of cells at once, typically there will only exist one more cell than can be shown. When a cell is moved off the screen it is placed in a pool of cells for reuse. Just before a cell moves onto the screen it is configured by you, in cellForRowAtIndexPath. If you have configured something in the cell and you do not configure that explicitly every time you return a cell from cellForRowAtIndexPath then that configuration persists in the cell that is in the reuse pool. The function prepareForReuse is also called before each cell is reused - if you have subclassed UITableViewCell then you can implement this function to return the cell to a base configuration, so that settings like text color do not unexpectedly affect multiple cells.
This approach makes it possible to scroll through an entire table view smoothly with a minimum amount of memory used. It is an expensive operation to create and destroy cells every time one disappears and a new one is required.
The simplest fix is to always set the text color in cellForRowAtIndexPath - either to the base color or to the special color you want in some cells.

How can I make the uicollectionview not dequeue the cells?

I don't want cells to have to be generated when they come on screen for the first time. I also don't want cells that have gone off screen to have to be regenerated when they come back on the screen.
How can I have all the cells generated on loading the UICollectionView and stay that way as I scroll up and down?
You could manage your own list of cells (pre-create them based on the datasource, create any new ones as the datasource updates, and return the appropriate cell for the indexPath desired instead of dequeuing a cell inside cellForItemAtIndexPath) but as a general rule, that's a bad idea. You may have some specific case where cell configuration performance is poor, but the answer is usually "improve cell configuration" and rarely "keep everything in memory".
Speculating: If you're thinking of doing this in order to preserve state or cache some information in the cell, that's the wrong place to put it. The cell is presentation; keep that info in the datasource element so that whatever needs to be represented can be applied to whatever cell is being used right now.

Resources