UITableViewCell Custom content re-added when cell becomes visible - ios

I am adding a custom button into my cell.contentView, and I noticed that every time a cell is scrolled off the visible part of the screen and back on, the button gets re-added - the translucent parts of it get more and more solid. What is the correct way to handle it so that it does not keep stacking more objects on top when scrolling through the tableView? Note that custom content is different for each cell, so I cannot put it into the if (cell == nil) {...} block.
The code I have is:
UISegmentedControl *btn = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObject:btn_title]];
// set various other properties of btn
...
[cell.contentView addSubview:btn];

Every time the cell is dequeued, you have to remove the old subviews before adding new ones, or else you'll get that stacking effect. You can do this in one of two places:
a) In tableView:cellForRowAtIndexPath:, remove your old views after the dequeueReusableCellWithIdentifier: call and before adding your new ones.
b) If you're using a subclass of UITableViewCell, you can override prepareForReuse to remove unwanted views. prepareForReuse is called every time a cell is dequeued for reuse, so it's a good place to get rid of old views from the last time the cell was configured.

I'll post a sample fix for the code you posted. It can be extended to take care of more views.
The steps are:
Create a method in your CustomCell class that takes care of the whole setup (for example: setupWithItems:)
Once you have a cell in cellForRowAtIndexPath: (after dequeueing it or creating it), you should call setupWithItems: with the new list of items the cell should display.
In your setupWithItems: implementation, make sure you remove the UISegmentedControl from its parent view. You can easily do this it the segmented control is stored as a property of your custom cell.
In your setupWithItems: implementation, create a new UISegmentedControl and add it to the CustomCell's view hierarchy.
Sample code:
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
CustomCell* cell = [tableView dequeueReusableCellWithIdentifier:kSomeIdentifier];
if (!cell)
{
// Create a new cell
}
NSArray* currentCellItems = [self cellItemsForRow:indexPath.row];
[cell setupWithItems:currentCellItems];
return cell;
}
And in your CustomCell subclass:
- (void)setupWithItems:(NSArray*)items
{
if (self.segmentedControl)
{
[self.segmentedControl removeFromSuperView];
self.segmentedControl = nil;
}
// More setup...
UISegmentedControl *btn = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObject:btn_title]];
// set various other properties of btn
[cell.contentView addSubview:btn];
}

Related

Create a UISwitch in one tableview cell - UISwitch gets duplicated

I have 11 or more number of rows. Need to create a UISwitch only in the first cell. The UIswitch gets duplicated when i click on any row.
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:EN_MoreTableViewCell];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:EN_MoreTableViewCell];
cell.backgroundColor = [UIColor clearColor];
}
cell.textLabel.text = languageObject.name;
[cell.textLabel setFont:font];
if (indexPath.row == 0 && [languageObject.name isEqual: #"All Languages"]) {
if (!mySwitch) {
mySwitch = [[UISwitch alloc] initWithFrame:CGRectMake(_languageListTableView.frame.size.width - 60, 0, 40, 40)];
[cell.contentView addSubview:mySwitch];
}
cell.accessoryType = UITableViewCellAccessoryNone;
}
else { //other cell code//
}
Please help.
This is a classic iOS newbie question. This confuses the hell out of most of us when we first use table views (it certainly confused me at first.)
Cells get created once and then reused over and over. The function dequeueReusableCellWithIdentifier() returns a recycled cell if one is available, or creates a new one from your cell prototype if not.
If you get a recycled cell, it will already have your switch added.
The cleanest way to handle this is to define a cell prototype using a custom subclass of UITableViewCell that has all your fields already added and connected as outlets to the cell. Then when you dequeue a cell, simply cast it to your custom UITableViewCell class and use the properties you've defined to access your custom fields (Your switch, in this case.)
A recycled cell may also contain values in it's other fields (Like if you've set a label field to contain a string, it will still contain the string.) You need to clear out old values and completely configure a recycled cell. (The custom cell class and prototype doesn't fix this problem. You always need to fully configure every field in your cell.)
Either:
Create two prototype cells in Storyboard, one with and one without UISwitch and dequeue the first only if indexPath.row == 0.
Or:
Add the UISwitchto your cell in Storyboard, make an IBOutlet to your cell and set self.mySwitch.isHidden = true in cells prepareForReuse().
This way the default state when reusing the cell is with hidden switch.
Later if indexPath.row == 0, set cell.mySwitch.isHidden = false.
It's because cells are reused. You can either remove all existing subviews in -[UITableViewDataSource tableView:cellForRowAtIndexPath:] or create a new cell for every row.
Sometimes a custom cell is a big hammer for just adding a single view to an otherwise perfectly good standard UITableViewCell. For those occasions, lazy creation is a nice pattern to get views built exactly once on reused cells (or even as any subview of any view). It works like this:
// in your cellForRowAtIndexPath, after dequeuing cell
UISwitch *switch = (UISwitch *)[cell viewWithTag:64]; // make up a unique tag
if (!switch) {
switch = [[UISwitch alloc] initWithFrame:...];
switch.tag = 64; // 64 must match the tag above
[cell addSubview:switch];
}
// here, switch is always valid, but only created when it was absent
Add a UISwitch in the storyboard. Connect outlet. In tableview's cellforrowatindexpath, if the index is 0 set hidden=false else set hidden=true. Hope this works.
You just hide the switch where you don't want to display and set the action for that switch dynamically for the particular indexpath you want

UIView is overlapping / creating again in cellForRowAtIndexPath

I have UITableViewCell that contains UIView (lets call it CPView) which is created while cellForRowAtIndexPath is called. CPView is just a plain coloured view and for every cell its width is different (that's why needed to create in cellForRowAtIndexPath).
Problem is
1)The CPView 's colour gets darker every time cell loads (May be due to every time that cell creates the same view so overlapping effect).
2) The cell overlaps / inherits other cell's CPView (we can see this because of light and dark colour of two CPView).
How can I prevent cell to recreate if it already exist or creation of this CPView again?
Edit
- (void)configureCell:(CreditDebitCell *)cell atIndexPath:(NSIndexPath *)indexPath {
//other code
UIView * CPView;
if (CPView){
CPView =nil;
}
else
{
CPView = [[UIView alloc] initWithFrame:CGRectMake(cell.bounds.origin.x, cell.bounds.origin.y, cell.frame.size.width*[self.percentArray[indexPath.row] floatValue] ,cell.frame.size.height )];
[CPView setClipsToBounds:YES];
[CPView setBackgroundColor:[UIColor colorWithRed:107/255.0 green:15/255.0 blue:47/255.0 alpha:0.5]];
[cell addSubview: CPView];
}
}
The issue here is reuse of the cells - and therefore you get multiple views added to your cell view.
You can:
-remove subview
-check if subview exists and do/don't do anything.
You can check if the subview is there by going through subviews:
for (UIView *v in cell.contentView.subview) {
if ([v isKindOfClass:[CPView class]]) {
// remove or flag that it exists
}
}
But I think that you should handle this in your cell - not your view controller that implements table view delegate. Better tell cell to use some view/hide some view based on some kind of logic then to do that inside cellForRowAtIndexPath
According to your i question(without cellforRowAtIndexpath) i can assume that you should check every time something like in cellForRowAtIndexPath
if(cpView){
cpView = nil;
}
// alloc again with required size for particular row.
Make a subclass of your UITableViewCell and make a property of it that will reference your CPView. This will now let you have a better control whether your subclassed cell does / doesn't have any CPView that needs to be added.

tableView reloadData will not remove previous UITextView that is programmatically placed

I've got a UITableView with several different elements added programmatically. The one I'm having trouble with is the UITextView that displays correctly with correct color, size, font, etc... I have a button in one cell that increases the size of the font in the UITextView in another cell. It works fine and has no issues. The numerical value is placed in a Plist, and when you leave the view with the table and come back the size changes perfectly.
I've placed a reloadData in the button which does reload the table and gives the textView new size and resizes it to fit the new content plus resizes the cell perfectly. The issue I'm having is that when the reloadData is called, the old textView remains. So I have two texts, at two different sizes, or three or four and so on. How can I remove the previous textView when it's not set to global?
Everything is set up exactly how one would expect:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
// cell with textView. Everything is instanced and created for just that cell with tags
UITextView *t = [self setSizeAndTextOfTextView];
[cell.contentView addSubview:t];
// cell with button. simple, alloc's and init inside cell. Calls method in same class
cell.contentView addSubview:button];
//method to increases font size
write to Plist the new size
[self.tableView reloadData]; <-- tableView is iboutlet that does reload table
How are you getting the cell in the first place? Are you reusing? If you are you don't want to add the textview as a subview again you want to retrieve the existing one and adjust it
UPDATE:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
if ([cell.contentView viewWithTag:1]) {
UITextView *t = (UITextView *)[cell.contentView viewWithTag:1];
//This version will take an existing textview and just resize it
[self setSizeAndTextOfTextView:t];
} else {
//This version creates a new text view
UITextView *t = [self setSizeAndTextOfTextView];
t.tag = 1
[cell.contentView addSubview:t];
}
You'll probably need to do something similar with you button as well
The reloadData won't wipe the existing cells, just the data displayed, so you'll get an old one to reuse
You may consider creating a custom subclass of UITableViewCell and associate that with your cell identifier. In your subclass, override the prepareForReuse method to set the cell back to a neutral state. Since cell objects are reused but are only initialized once, prepareForReuse is available to restore an already existing cell to its freshly initialized state.

Reuse of UICollectionViewCells during scrolling

I'm having an issue,
I have a simple UICollectionView with a static 200 cells that load images from Flickr.
my CellForItemAtIndexPath looks like this:
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];
if(![[cell.subviews objectAtIndex:0] isKindOfClass:[PFImageView class]])
{
NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
PFImageView *imageView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell];
[cell addSubview:imageView];
}
return cell;
}
PFImageView is a subclass of UIImageView that loads a Flickr photo URL on a background thread and then updates it's own image on the main thread - this works fine.
The logic is really simple - I create a cell if there isn't one dequeueable.
If the cell (which I'm expecting to be dequeued and already have a PFImageView) doesn't have a PFImageView, I alloc and init an imageView for the cell and add it as a subview of the cell.
Thus I expect if the cell has been dequeued it should already have a PFImageView as a subview and as we should not get into the if statement to create a new imageView and kick off a new photo download request
Instead what I see is that the cells at the top and bottom of the UICollectionView that 'go off screen' momentarily - when they come back on screen they are not being reused and seemingly a new cell is created and the picture refreshed.
1) How can I achieve a static image once the cell has been created (i.e. not refreshing when the cell goes slightly off screen.
2) Why are the cells not being reused?
Many thanks for your time.
John
UICollectionView will reuse cells for maximum efficiency. It does not guarantee any particular reuse or population strategies. Anecdotally, it seems to place and remove cells based on integer power of two regions — e.g. on a non-retina iPad it might divide your scroll area up into regions of 1024x1024 and then populate and depopulate each of those regions as they transition into and out of the visible area. However you should not predicate any expectations on its exact behaviour.
In addition, your use of collection view cells is incorrect. See the documentation. A cell explicitly has at least two subviews — backgroundView and contentView. So if you add a subview it will be at index 2 at the absolute least and, in reality, the index will be undefined. In any case you should add subviews to contentView, not to the cell itself.
The most normal way of doing what you're doing would be to create a custom UICollectionView subclass that inherently has a PFImageView within it.
I see several potential issues:
You are looking specifically at index 0 of the cell for the child class that you are adding. The UICollectionViewCell may have other views as children, so you can't just assume that the only (or first) child is the one you added.
I don't see that you are calling registerClass:forCellWithReuseIdentifier: or registerNib:forCellWithReuseIdentifier:, one of which is required for proper use of dequeue (https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewCell_class/Reference/Reference.html).
You are only setting the URL of the PFImageView in the case that you have to construct the PFImageView. The idea with dequeuing reusable views is that you will only construct a small subset of the views needed, and the UITableView will recycle them as they move offscreen. You need to reset the value for the indexPath that is being requested, even when you don't construct the new content.
If your case is as simple as you describe, you can probably get away with adding your PFImageView to the contentView property of your dequeued UICollectionView.
In your controller:
// solve problem 2
[self.collectionView registerClass:[UICollectionViewCell class] forReuseIdentifer:#"FlickrCell"];
In collectionView:cellForItemAtIndexPath
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];
// solve problem 1 by looking in the contentView for your subview (and looping instead of assuming at 0)
PFImageView *pfImageView = nil;
for (UIView *subview in cell.contentView.subviews)
{
if ([subview isKindOfClass:[PFImageView class]])
{
pfImageView = (PFImageView *)subview;
break;
}
}
NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
if (pfImageView == nil)
{
// No PFImageView, create one
// note the use of contentView!
pfImageView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.contentView.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell.contentView];
[cell.contentView addSubview:pfImageView];
}
else
{
// Already have recycled view.
// need to reset the url for the pfImageView. (Problem 3)
// not sure what PFImageView looks like so this is an e.g. I'd probably remove the
// URL loading from the ctr above and instead have a function that loads the
// image. Then, you could do this outside of the if, regardless of whether you had
// to alloc the child view or not.
[pfImageView loadImageWithUrl:staticPhotoURL];
// if you really only have 200 static images, you might consider caching all of them
}
return cell;
For less simple cases (e.g. where I want to visually lay out the cell, or where I have multiple children in the content), I typically customize my UICollectionViewCell's using Interface Builder.
Create a subclass of UICollectionViewCell in the project (In your case, call it PFImageCell).
Add an IBOutlet property to that subclass for the view I want to change in initialization (In your case, a UIImageView).
#property (nonatomic, assign) IBOutlet UIImageView *image;
In Interface Builder, create a prototype cell for the UITableView.
In the properties sheet for that prototype cell, identify the UICollectionViewCell subclass as the class.
Give the prototype cell an identifier (the reuse identifier) in the property sheet.
Add the view child in interface builder to the prototype cell (here, a UIImageView).
Use IB to map the IBOutlet property to the added UIImageView
Then, on dequeue in cellForRowAtIndexPath, cast the dequeued result to the subclass (PFImageCell) and set the value of the IBOutlet property instance. Here, you'd load the proper image for your UIImageView.
I am not sure if the cell is being re-used or not. It may be being reused but the subview may not be there. My suggestion would be to create a PFImageViewCollectionViewCell Class (sub class of UICollectionViewCell) and register it as the CollectionView Cell and try. That's how I do and would do if I need a subview inside a cell.
Try adding a tag on this particular UIImageView
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static int photoViewTag = 54353532;
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];
PFImageView *photoView = [cell.contentView viewWithTag:photoViewTag];
// Create a view
//
if (!photoView) {
photoView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell];
imageView.tag = photoViewTag;
[cell.contentView addSubview:imageView];
}
// Update the current view
//
else {
NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
photoView.imageURL = staticPhotoURL;
}
return cell;
}
I would really recommend to create your own UICollectionViewCell subclass though.
EDIT: Also, note that I used the contentView property instead of adding it directly to the cell.

UITableViewCell's contentView's content not visible when using UISearchDisplayController

I have written a simple contact manager application that uses a UITableView to display the contacts. Each contact is shown as a standard UITableViewCell; custom content is created as UIButtons and UILabels that are added as subviews of the cell's contentView. My table viewController's cellForRowAtIndexPath method includes:
UIButton *emailButton;
UITableViewCell *cell =
[theTableView dequeueReusableCellWithIdentifier:#"My Identifier"];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:identifier] autorelease];
emailButton = [UIButton buttonWithType:UIButtonTypeCustom];
[emailButton setImage:emailImage forState:UIControlStateNormal];
emailButton.tag = EMAIL_BUTTON_TAG;
emailButton.frame = emailButtonFrame;
[cell.contentView addSubview:emailButton];
} else {
emailButton = (UIButton *)[cell viewWithTag:EMAIL_BUTTON_TAG];
}
... set various attributes of the cell, including the content of custom labels
... added as subviews of the contentView exactly as above
This works fine when rendering my table. But I've also added a search bar to my app, set the search bar's controller appropriately, and set the controller's delegate back to this same tableController such that the exact same cellForRowAtIndexPath method is called when performing the searches, and of course I filter the set of cells to be displayed to match the query.
What I see is that when I perform a search, all of the content that I display by setting cell.textLabel.text or cell.imageView.image shows up perfectly in the table, but the emailButton or the labels that I added as subviews of the cell's contentView don't appear. In the debugger, I can clearly see that these controls (the buttons and labels) exist at the time that cellForRowAtIndexPath is called while search filtering is going on. But the controls don't render.
I feel there must be something very subtle in the interactions between table cells and the searchView, but I'm missing it.
Setting the textLabel's text property appears to also bring the textLabel to the front. Even though the text label does not appear to overlap with any of the content view's buttons, this is causing the buttons to disappear. Forcing them to the front after the textLabel is updated makes the problem go away.
It is not clear why this behavior is only appearing in the search case and not in the normal case, but I was able to reproduce it in a simple change to the iOS "TableSearch" example.
you can check if
identifier in *cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:identifier] autorelease];
is equalt to "My Identifier" from [theTableView dequeueReusableCellWithIdentifier:#"My Identifier"]; if not you can get empty cell, which you can use it's cell.imageView and cell.textLabel but does not have the contentView subviews.

Resources