My setup
I have a UITableViewCell that is in my main storyboard in a UITableViewController. It gets populated with some JSON data pulled from a REST API that will cause each cell to be a variable height. There are UIImageViews, UILabels all of different heights and styles, think Instagram-esque.
My problem
When I scroll to maybe the 5th or 6th cell, then go back up, they start redrawing and overlapping, so text gets mixed, lines get redrawn, etc.
What I've tried
This seems like a common problem on SO, so I've tried several posted solutions. It seems like my issue is probably the same problem as others face, which is, I am calling addSubview on my cell every time it dequeues, but I've tried checking to see if the cell already exists. I came across another post somewhere (sorry, I can't remember where), that suggests that because I am creating this in the storyboard, it is already initialized and if ( !cell ) will already return false, so I don't know how to prevent it from redrawing.
When I try removing the cell from the storyboard, and creating it programmatically, I get an error saying it can't find a cell with my identifier #"Cell".
I've also tried someone's solution of removing all subviews when I dequeue, so I used:
for ( UIView *view in cell.contentView.subviews ) {
if ([view isKindOfClass:[UIView class]]) {
[view removeFromSuperview];
}
}
and it doesn't find anything.
#rdelmar's comment is correct. You shouldn't do what you're doing. Might work, but it's bad form and you don't want to get into bad habits.
First, take advantage of object oriented programming. A cell should be able to configure itself based on the data you ask it to display. The table view shouldn't be designing the cell.
UITableViewCells need to be optimized for speed. Creating and adding subviews is a slow process. It's OK to do it once, but the cell will be reused (a system optimization) and you should just reuse the existing views that were added the first time the cell was created.
For example, you can hide subviews if they're not needed. You might want to do this in -prepareForReuse. You can move them around in -layoutSubviews. Or change the position of subviews in -updateConstraints.
Typically you just want to pass the data to display to the table view cell subclass from the data source (often the view controller). Let the cell do the display work.
When you add your subview after dequeueing uour cell, give a tag to your subview. This way, when you dequeue a cell, you can first check for the presence of a subview with your tag, and if it exists, remove it before adding your new view:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
// try to dequeue a cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:<yourCellIdentifier>];
if( !cell )
{
// create a new cell if necessary
}
static int _myViewTag = 1000987 // give it a high int : low value are used by the system in cells
UIView *v = cell.contentView viewWithTag:_myViewTag];
if( v ) // subview with such tag already exists, so remove it.
[v removeFromSuperview];
// now add your new subview
[cell.contentView addSubview:<yourView>];
// adjust height of cell to your view.
...
}
Try to add a new method in your cell class to reset cell to its default style and call this method after dequeueCell.
The most efficient way to manage this is to subclass UITableViewCell and adding all your required Views as properties. So now when a cell comes up for "recycling", you know where to find the old views, like :
[cell.myTextLabel setText:#""];
aaaand you're done.
UPDATE creating a subclass makes sense if you have only a small number of "TYPES" of cells. create a subclass for each. How much complicated it gets depends on your specific scenario. But i've done it and found it to be the most effective method.
UPDATE 2 or you could make multiple cells in the storyboard, and dequeue the appropriate one based on the data source, save all the coding.
Related
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() }
I have tried to create a custom cell that would display a list of items. To achieve this I tried to create a custom cell with UITableView inside it with scroll disabled.
The problem I have with this is when i try to apply changes to the cells in the inner UITableView data just does not get updated and the cell stays as it was. I have tried calling [tableView reloadData], [cell setNeedsDispaly], [cell setNeedsLayout] to no avail.
It seems like the data that has been applied when the cell was initialised persists through any attempts to change it. Though, when I create a breakpoint in cellForRowAtIndexPath: the data does get updated but is not rendered.(e.g. text property of UILabel has new value, but text is old on the screen.)
You need to nil the cell and then reload the table view. For some reason iOS caches table view data and stuff doesn't get updated correctly.
You could try overriding UITableViewCell's prepareForReuse(): in that method you can set all the cell's outlets/properties to nil. You would do this for the cells in the main cell's table view. I am assuming those are custom cells as well and that you have a custom UITableViewCell subclass for them.
Refer [I have two views on one cell, when I click on a cell it will be hidden and one edit form will be expanded on that. How to resolve that? ..check in accepted answer methods ...
[tableView reloadRowsAtIndexPaths:ll withRowAnimation:UITableViewRowAnimationAutomatic] in didSelectRowAtIndexPath
Before I get crucified for this, let me state... Yes, I have read every "relevant" answer on this topic and have not found a workable solution. Most "correct" answers are pre-ARC and discuss "releasing" a cell, which just isn't done anymore. Secondly, my problem is not "global", meaning some views have no problems, while others do. So here is my question...
I have sub-classed uitableviewcell and setup some uilabels & custom uiviews. From there I wired everything up in ib (Xcode 5.x iOS 7.x). Once I put in the appropriate code and create the tableview & dynamic cells from a nsarray "not mutable" everything works exactly as expected with no issues.
This is the fun part. I am making changes to allow the data source of the tableview, which is an nsarray to be mutable to allow adding and removing of items / cells. This is where things get hairy. When I start to add more objects to the array and when the reuse cell is being put on screen visual data from old cells is being reused on new cells. I say "visual" because once the cell is selected the view updates to display the correct information. The part that is interesting is that as I stated I have some uilabels which never have any problems being redisplayed, my custom views however are now the piece of the puzzle that is displaying info from past cells, and when scrolling back up, the original cells no longer display the correct information. Once the cell is tapped, then the cell updates and displays the correct information.
the most confusing bit of this is that before my array was mutable and had a static amount of objects this worked fine. Even if a cell went off screen and came back, it was still the correct information being displayed. Now I know that shouldn't have anything to do with it, but it is strange that it worked using the same tableview & cell code that I am using now.
I have tried adding in
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
if (!cell) {
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"]; // note: obviously as stated, tacking on "autorelease" here as mention in other suggestions is not going to work.
}
Which doesn't fix the issue.
I tried overriding the "prepareForReuse" method on my custom cell subclass and that does not resolve the issue either. I have made the views, "strong" & "weak", and all that and still every 3rd or so cell gets repeated with garbage data until it is refreshed. Again, the uilabels which are setup the same way as the views have no problems and data is never reused. I would say there is a problem with my custom views, but setting up the table from a static source of identical information there is no problem.
I would like to post some code, but it's all pretty generic code for tableviews & delegates. any suggestions would be greatly appreciated.
As i said the code is all pretty generic, but apparently it needs posing anyway so here it is..
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyThing *thing = self.stuffArray[indexPath.row];
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.thisLabel.text = thing.someText;
cell.thatLabel.text = thing.otherText;
cell.view1.someProptery = thing.object1.property
cell.view2.someProptery = thing.object2.property
cell.view3.someProptery = thing.object3.property
//"someProperty" on "view..." is an NSInt that is used to determine custom drawing in the view.
return cell;
}
I think the key to the solution lies in your comment about the custom views in the cells. If cellForRowAtIndexPath is altering the states of those views, they need to know that they must be redrawn, so you'll need to augment the synthesized setter in your custom view.m that has someProperty.
If the someProperty determines how this view get's drawn, then it's incumbent upon the setter to indicate that the view is out of date....
- (void)setSomeProperty:(NSInteger)someInt {
_someProperty = someInt;
[self setNeedsDisplay];
}
I'm having trouble understanding how this works. I've read many threads on SO about it - such as UITableView dequeueReusableCellWithIdentifier Theory and How does dequeueReusableCellWithIdentifier: work?.
However, my UITableView succesfully dequeues a cell each time (it's never nil), even when it first loads. I was under the impression that similar cells should use the same identifier, so you only have to change what's necessary.
Because
if (!cell) {
NSLog(#"New cell");
cell = [[UITableViewCell alloc] initWithStyle:someStyle reuseIdentifier:someIdentifier];
}
Never gets called, I'm not sure how I'm supposed to handle cells in the same table with different styles, because the style can only be set in the initializer.
I also tried using different cell identifiers, to make sure it wasn't reusing cells from a different table or something. I am registering these identifiers with [tableView registerClass: forCellReuseIdentifier:]
If I understand, this method should only return previously created cells that have been moved off the screen (hidden, i.e. can be reused). Then how come it returns a cell the first time it's called?
Edit: So the confusion was using [tableView dequeueReusableCellWithIdentifier: forIndexPath:] instead of [tableView dequeueReusableCellWithIdentifier:] (the first requires registering the identifier, the second will return nil if none is available - the behavior I was expecting above).
However, I noticed that when I changed my code to use [tableView dequeueReusableCellWithIdentifier:], it creates a new cell, and its contentView.frame has a width of 320 (full width). Before, when I did dequeue...forIndexPath it would give a width of 302, or the visual/"real" width of the cell. Why is this?
Also, is there a way to specify the style of the UITableViewCells regstiered for reuse?
Solution: So I found this thread UITableView cell.contentView.bounds.size.width Changes With Cell Reuse, which says when you set the autoresizingmask to UIViewAutoresizingFlexibleLeftMargin, it's fixed when you try to do relative positioning (the contentView width is initially the fully width, but when you present it it's shrunk, so if you do your calculations right it'll still show up properly).
I was positioning a UISwitch on the right - and when I set the autoresizing mask it works when it's first displayed but shifted over another ~20 pixels when I switched it. I don't know what caused that extra shift, but I ended up solving it by simply setting the UISwitch as the cell's accessoryView.
(This is partially off topic from the original question, but if someone stumbles on this maybe it'd be useful). For anyone wondering specifically about the original question, the answer is under the first edit.
When you call [tableView registerClass: forCellReuseIdentifier:], you're teaching the table view what to do when you later use the specified ReuseIdentifier. So, when you later call [tableView dequeueReusableCellWithIdentifier:] it will either:
A. Get a cell that has previously been created and isn't currently being used
OR
B. Create a new cell of the class you specified
So, when you dequeue, you will always get an instance. If you want to create new cell instances yourself with initWithStyle:reuseIdentifier: then you shouldn't register a class with the table view. Alternatively, leave the registration and add logic to specify everything that needs to be configured (and consider using multiple different cell classes and reuse identifiers).
because the first time the cell is nil that is why this gets called:
if (!cell) {
NSLog(#"New cell");
cell = [[UITableViewCell alloc] initWithStyle:someStyle reuseIdentifier:someIdentifier];
}
but then if the cell is already ready for reuse and basically its not nil - it returns the cell and it does not hit the above if statement
From the apple docs at https://developer.apple.com/library/ios/#documentation/UIKit/Reference/UITableView_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006943
Call this method from your data source object when asked to provide a new cell for the table view. This method dequeues an existing cell if one is available or creates a new one using the class or nib file you previously registered. If no cell is available for reuse and you did not register a class or nib file, this method returns nil.
The dequeue method will
Return a recycled cell if one is available
Create a new cell if you registered one (you mentioned you did this)
If none of these are true, it returns nil
I'm guessing if you remove the registration (which may be hidden in a xib) then you will get the nil result.
if you see UITableView.h
Beginning in iOS 6, clients can register a nib or class for each cell.
If all reuse identifiers are registered, use the newer -dequeueReusableCellWithIdentifier:forIndexPath: to guarantee that a cell instance is returned.
Instances returned from the new dequeue method will also be properly sized when they are returned.
(void)registerNib:(UINib *)nib forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0);
(void)registerClass:(Class)cellClass forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);
I have a static UITableView built from a Storyboard that works well. I want to fill the first category programmatically, though, from a user-defined file
Simply put, I want to go through all the strings in an array and add them as cells for the rows of the first category. For the second category, I have a series of mildly complex cells (containing a number of labels, textfields, buttons and other controls), defined in the storyboard, that I don't feel like recreating in code.
As far as I understand, the default behaviour for a UITableView built from a storyboard is to use the nib file as an implicit datasource. If I use a custom class as datasource, my second section doesn't work. I have thought of two possible ways to fix this:
Fill my first category from the datasource and delegate the rest to the nib file. Is this possible? Is there some method to programmatically ask the nib to fill my UITableView?
Export my storyboard-built cells into code and paste this code into my datasource. This method has the disadvantage of making my second category harder to modify.
Is one of those two options feasible? Is there another option?
I would use dynamic prototype cells. Then, I would set up the ViewController as the delegate and the dataSource. I would then create a custom subclass of UITableViewCell and connect the elements of the second section to IBOutlets in the custom UITableViewCell.
If the first section wasn't something that could be done with one of the generic cell types, I would also create a custom subclass of UITableViewCell for that section as well.
I would then use the cellForRowAtIndexPath: method to set up the cells with the information that I want in them. So if my first section used FirstSectionCell and my second section used SecondSectionCell as custom subclasses of UITableViewCell my cellForRowAtIndexPath: would look like this:
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.section==0)
{
FirstSectionCell *firstCell = [tableView dequeueReusableCellWithIdentifier:#"First Cell Prototype"];
//Set up the first cell.
return firstCell;
}
else if(indexPath.section ==1)
{
SecondSectionCell *secondCell = [tableView dequeueReusableCellWithIdentifier:#"Second Cell Ptototype"];
//Set up second cell.
secondCell.someLabel.text = #"whatever";
//etc.
return secondCell;
}
else
{
//if you have another section handle it here.
}
}
There are two kinds of table views when you use Storyboards:
Static
Dynamic
You're currently using the former. You define everything in the Storyboard and have very little code.
But you need to change to the latter.
You can still keep your UITableViewCells in the Storyboard; there's no need to do that in code (though you can if it makes things easier). You can refer to the template cells using the "reuse identifer."
Otherwise you've pretty much got it. You'll need to write code to implement the data source and (possibly) more methods of the table view delegate.
It's kind of fiddly switching from static to dynamic. I keep meaning to raise a Radar because I'm sure Xcode could be making it easier to do...