I'd like to add a button to a table cell. The "Delete Event" in the calendar app inspired me... (a similar case is "Share Contact" in contacts)
As of now there's
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//..yadayadayada
cell = [tableView dequeueReusableCellWithIdentifier:#"buttonCell"];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:#"buttonCell"] autorelease];
}
UIButton *button = [UIButton buttonWithType:UIButtonTypeInfoDark];
[button setBackgroundColor:[UIColor redColor]];
button.titleLabel.text = #"Foo Bar";
[cell.contentView addSubview:button];
which produces a button, indeed. It doesn't look yet how it's supposed to (it's obvious I've never dealt with buttons in iPhone, yet), but is this at least the right approach?
The way you have it now each time a cell is shown you're allocating a button, setting its value, and adding it to the cell's contentView. When the cell gets reused (via dequeueReusableCellWithIdentifier) you'll be creating another new button, adding it to the cell (on top of the old one) etc. The fact that it's gone through addSubview but no explicit release means each button's retain count will never go to zero so they'll all stick around. After a while of scrolling up and down the cell will end up with hundreds of button subviews which probably isn't what you want.
A few tips:
Never allocate stuff inside a cellForRowAtIndexPath call unless it's done when dequeueReusableCellWithIdentifier is returning nil and you're initializing the cell. All other subsequent times you'll be handed back the cached cell that you will have already set up so all you have to do is change the labels or icons. You're going to want to move all that button allocation stuff up inside the if conditional right after the cell allocation code.
The button needs to have a position and also a target set for it so it'll do something when tapped. If every cell is going to have this button a neat trick is to have them all point to the same target method but set the button's tag value to the indexPath.row of the cell (outside the cell allocation block since it varies for each cell). The common tap handler for the button would use the tag value of the sender to look up the underlying data in the dataSource list.
Call release on the button after you've done an addSubview. That way the retain count will fall to zero and the object will actually get released when the parent is released.
Instead of adding the button via addSubview, you can return it as the accessoryView for the cell so you don't have to worry about positioning it (unless you're already using the accessoryView for something else -- like disclosure buttons).
I took a different approach to creating an equivalent to the 'Delete Event' button in the Calendar app. Rather than add a button as a subview, I added two background views (red and darker red, with nice gradients) to the cells and then rounded off the corners and set the border to grey.
The code below creates a re-usable cell (in the usual fashion). The two images referred to ('redUp.png' and 'redDown.png') were taken from a screenshot of the calendar's 'Delete Event' button. (That seemed quicker than creating the gradients programmatically.) There's scope for a bit more fine tuning to get it even closer to the Calendar's 'Delete Event' appearance, but this is pretty close.
The button's action is triggered by the tableView delegate method tableView:didSelectRowAtIndexPath: method.
// create a button from a table row like the Calendar's 'Delete Event' button
// remember to have an #import <QuartzCore/QuartzCore.h> some above this code
static NSString *CellWithButtonIdentifier = #"CellWithButton";
UITableViewCell *cell = [self dequeueReusableCellWithIdentifier:CellWithButtonIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellWithButtonIdentifier] autorelease];
[[cell textLabel] setTextAlignment: UITextAlignmentCenter];
UIImageView* upImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"redUp.png"]];
UIImageView* downImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"redDown.png"]];
[cell setBackgroundView: upImage];
[cell setSelectedBackgroundView: downImage];
[[upImage layer] setCornerRadius:8.0f];
[[upImage layer] setMasksToBounds:YES];
[[upImage layer] setBorderWidth:1.0f];
[[upImage layer] setBorderColor: [[UIColor grayColor] CGColor]];
[[downImage layer] setCornerRadius:8.0f];
[[downImage layer] setMasksToBounds:YES];
[[downImage layer] setBorderWidth:1.0f];
[[downImage layer] setBorderColor: [[UIColor grayColor] CGColor]];
[[cell textLabel] setTextColor: [UIColor whiteColor]];
[[cell textLabel] setBackgroundColor:[UIColor clearColor]];
[cell setBackgroundColor:[UIColor clearColor]]; // needed for 3.2 (not needed for later iOS versions)
[[cell textLabel] setFont:[UIFont boldSystemFontOfSize:20.0]];
[upImage release];
[downImage release];
}
return cell;
Yes, this is generally the correct approach.
A tip:
Set the callback for your button events, so that it actually does something when clicked.
[myButton addTarget:self action:#selector(whatMyButtonShouldDo:)
forControlEvents:UIControlEventTouchUpInside];
EDIT: Adjusted code for using a system button, which renders a lot of what I had written uneccessary.
Yes, you are on the right track, that's the easiest way to add a subview to a cell (the other is subclassing a UITableViewCell).
Check the Apple guide for more info.
To avoid positioning and memory management hassles you can create a specialized cell class and link to a XIB. That sounds like the cleanest approach to me. Here is the link:
http://icodeblog.com/2009/05/24/custom-uitableviewcell-using-interface-builder/
Related
I am having trouble customizing the look and behavior of a subclassed tableview cell when it enters selected state.
My cell has three labels I added to its content view in the initWithStyle: method as such:
cell1Label = [[UILabel alloc] initWithFrame:
CGRectMake(75.0f, 12.0f, 67.0f, 12.0f)];
cell1Label.backgroundColor = [UIColor clearColor];
cell1Label.textColor = [UIColor blackColor];
cell1Label.shadowColor = [UIColor whiteColor];
blah, blah, blah...
[self.contentView addSubview:cell1Label];
Then, I put a black overlay on top of the background in the setSelected:(BOOL)selected animated:(BOOL)animated method within the subclass:
UIView *backgroundView = [[UIView alloc] initWithFrame:
CGRectMake(0.0f, 0.0f, 150.0f, 70.0f)];
backgroundView.backgroundColor = [UIColor colorWithRed:
0.0 green:0.0 blue:0.0 alpha:0.4];
self.selectedBackgroundView = backgroundView;
The problem start here. Because I want to keep my UILabel readable when the cell is selected, I need to change their textColor and shadowColor. However, I cannot seem to find a good place to do this.
If I put the code in the setSelected:(BOOL)selected animated:(BOOL)animated nothing happens; I can only seem to add changes to the selectedBackgroundView.
I also tried using the didSelectRowAtIndexPath: and didDeselectRowAtIndexPath: TableView delegate methods as such:
CustomDataCell* selectedCell = (CustomDataCell*)[tableView
cellForRowAtIndexPath:indexPath];
selectedCell.cell1Label.shadowColor = [UIColor lightGrayColor];
selectedCell.cell1Label.textColor = [UIColor blackColor];
This method, however, has some issues when cells leave the visible area. Namely, if I select a cell then it leaves the visible area, its text properties do not change back to their normal state when I select another cell. The black background disappears as it should, but the new textColor and shadowColor I assigned to the selected state persists.
What is the best, most reliable way to handle selected (and possibly other) states of subclassed UITableViewCells?
I am using ARC; never use IB; on Xcode 4.6 and iOS 6.1 SDK.
Use the setHighlighted:animated method of UITableViewCell to change your label color.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
// Change you label text color here
//
// Edit Here
if (selected) {
// New Colors Here
}
else {
// Old Colors Here
}
}
When a cell is selected, it will set all of its labels (including ones you've added yourself) to their highlighted state. This means you can define the highlighted text color at initialisation and let the cell take care of it.
Probably at the moment the labels are being set to highlighted which is undoing any colour changes you are making yourself.
Also, a cell has a selectedBackgroundView property which you should be using instead of adding a new subview.
In my app, I have a table view that has about eight cells. There is a navigation bar at the top. When a user touches a cell, nothing happens for about 1/2 second. Then the touched cell highlights blue and immediately the new view slides into position.
The problem is that there is no feedback to the user about which cell he touched until just before the new view slides into position.
For example, when I explore the tables in the iPhone's Settings application, when you touch a cell, the cell immediately turns blue, and then there is a 1/2 second delay, and then you see the new view.
How do I get my table's feedback of highlighting the cells to happen immediately? I am using tableView didSelectRowAtIndexPath:, and each cell has an accessory button.
Thanks for any insight.
are you using a custom-drawn cell (with a drawRect override) or something like that?
if you have a custom drawRect method, you'll need to do something like this (based off the code for tweetie, found here):
//default colors for cell
UIColor *backgroundColor = [UIColor whiteColor];
UIColor *textColor = [UIColor blackColor];
//on highlight, swap colors
if(self.highlighted){
backgroundColor = [UIColor clearColor];
textColor = [UIColor whiteColor];
}
that should give you the default behavior.
It sounds like the screen is refreshing after the new slide has been processed. You need to refresh the screen before rendering the new slide view.
a few things:
it's probably best to have a property in beforeViewController so it can set its own title on load (instead of setting it from the parent class).
second, why are you setting the back button for the current class? youre also leaking that (you alloc the UIBarButtonItem but dont release it).
NewViewController *newViewController = [[[NewViewController alloc]
initWithNibName:#"New" bundle:nil] autorelease];
newViewController.name = [self.listData objectAtIndex:indexPath.row];
[self.navigationController pushViewController:beforeAfterViewController animated:YES];
then in NewViewController, you have
- (void) viewDidLoad{
self.title = self.name;
}
re: your secondary question: if you pop the child controller using [self.navigationController popViewControllerAnimated:YES], the parent view should auto-deselect the row that was previously selected. it shoulnt stay selected unless you are forcing it to stay that way.
you don't need to do anything like [self.tableView deselectRowAtIndexPath:] unless you are not pushing child views (and doing something like checkmarking a cell that the user tapped).
You may put these 2 lines of code at the beginning of the didSelectRowAtIndexPath method:
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[cell setSelected:YES animated:YES];
it should first highlight the cell before processing other program logic.
How can I determine what UIImageView I touch in the screen? Like for example, I added 10x10 tiled UIImageView in a UIView. Now in touchesBegan, how can I know the image I touch in screen? Should I use hitTest method in this implementation?
Thanks.
I generally find the easiest way is to subclass UIImageView and add my own touch event handlers.
In fact what I have often done is create one subclass of UIImageView who's sole purpose is to allow another class to act as next responder. That way I don't have to subclass it for every situation. That's only worth doing if you need a number of these views and you weren't subclassing them anyway.
this question was asked many times here. try to use search next time. I usually use UIButtons with custom style to do what you want. such a variant gives you standard methods to catch touches without subclassing.
To follow up on the UIButton suggestion, here's a snippet of what I have done for a UIButton that is rendered with a UIImage.
This was for tracking the specific UIButton image type that got touched within a UITableViewCell, and it passes along the row and section in which this UIButton was placed:
UIButton *_infoButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
[_infoButton setImage:[UIImage imageNamed:#"Info.png"] forState:UIControlStateNormal];
[_infoButton setTitle:[NSString stringWithFormat:#"%d:%d", indexPath.section, indexPath.row] forState:UIControlStateReserved];
[_infoButton addTarget:self action:#selector(touchedDetailedInfoButton:) forControlEvents:UIControlEventTouchDown];
// ...
[ _infoButton release];
And here's the associated method for recovering the title and doing something interesting with it:
- (void) touchedDetailedInfoButton:(NSString *)title {
unsigned int _section, _row;
const char * _indexPathCharPtr = [title cStringUsingEncoding:NSUTF8StringEncoding];
sscanf(_indexPathCharPtr, "%d:%d", &_section, &_row);
NSUInteger _path[2] = {_section, _row};
NSIndexPath *_touchedIndexPath = [[NSIndexPath alloc] initWithIndexes:_path length:2];
[self doSomethingInterestingWithIndexPath:_touchedIndexPath];
[_touchedIndexPath release];
}
There's probably an easier way to go about this, so hopefully someone else comes along and offers an alternative (other than subclassing UIImageView, which is a perfectly valid option, as well).
How can I set the background color of a cell in UITableView?
Thanks.
I know this is an old post, but I am sure some people are still looking for help. You can use this to set the background color of an individiual cell, which works at first:
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
[cell setBackgroundColor:[UIColor lightGrayColor]];
However, once you start scrolling, the iphone will reuse cells, which jumbles different background colors (if you are trying to alternate them). You need to invoke the tableView:willDisplayCell:forRowAtIndexPath. This way, the background color gets set before the reuse identfier is loaded. You can do it like this:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
cell.backgroundColor = ([indexPath row]%2)?[UIColor lightGrayColor]:[UIColor whiteColor];
}
The last line is just a condensed if/else statement. Good luck!
Update
Apparently the existing UITableViewCell framework makes it very difficult to change the background color of a cell and have it work well through all its state changes (editing mode, etc.).
A solution has been posted at this SO question, and it's being billed on several forums as "the only solution approved by Apple engineers." It involves subclassing UITableViewCell and adding a custom view for the subclassed cell's backgroundView property.
Original post - this solution doesn't work fully, but may still be useful in some situations
If you already have the UITableViewCell object, just alter its contentView's backgroundColor property.
If you need to create UITableViewCells with a custom background color, the process is a bit longer. First, you'll want to create a data source for your UITableView - this can be any object that implements the UITableViewDataSource protocol.
In that object, you need to implement the tableView:cellForRowAtIndexPath: method, which returns a UITableViewCell when given an NSIndexPath for the location of the cell within the table. When you create that cell, you'll want to change the backgroundColor property of its contentView.
Don't forget to set the dataSource property of the UITableView to your data source object.
For more info, you can read these API docs:
UITableViewDataSource - tableView:cellForRowAtIndexPath
UITableViewCell - contentView
UIView - backgroundColor
UITableView - dataSource
Note that registration as an Apple developer is required for all three of these links.
The backgroundView is all the way on the bottom. It's the one that shows the rounded corners and the edges. What you want is the contentView which is on top of the backgroundView. It covers the usually white area of the cell.
The version I wrote will work in iPhone 3.0 or higher and fallback to a white background otherwise.
In your viewDidLoad method of the UITableViewController we add the following:
self.view.backgroundColor=[UIColor clearColor];
// Also consider adding this line below:
//self.tableView.separatorColor=[UIColor clearColor];
When you are creating your cells (in my code this is my tableView:cellForRowAtIndexPath:) add the following code:
cell.backgroundColor=[UIColor colorWithPatternImage:[UIImage imageNamed:#"code_bg.png"]];
float version = [[[UIDevice currentDevice] systemVersion] floatValue];
if (version >= 3.0)
{
[[cell textLabel] setBackgroundColor:[UIColor clearColor]];
}
This works perfectly for me:
NSEnumerator *enumerator = [cell.subviews objectEnumerator];
id anObject;
while (anObject = [enumerator nextObject]) {
if( [anObject isKindOfClass: [ UIView class] ] )
((UIView*)anObject).backgroundColor = [UIColor lightGrayColor];
}
You may set the backgroundColor of the backgroundView. If the backgroundView does not exists, you can create one for it.
if (!tableView.backgroundView) {
tableView.backgroundView = [[UIView alloc] initWithFrame:tableView.bounds];
}
tableView.backgroundView.backgroundColor = [UIColor theMostFancyColorInTheUniverse];
If you want to set the background of a cell to an image then use this code:
// Assign our own background image for the cell
UIImage *background = [UIImage imageNamed:#"image.png"];
UIImageView *cellBackgroundView = [[UIImageView alloc] initWithImage:background];
cellBackgroundView.image = background;
cell.backgroundView = cellBackgroundView;
I have a UITableView with reorderable rows and I'm using the standard UITableViewCell.text property to display text. When I tap Edit, move a row, tap Done, then tap the row, the built-in UILabel turns completely white (text and background) and opaque, and the blue shade to the cell doesn't show behind it. What gives? Is there something I should be doing that I'm not? I have a hacky fix, but I want the real McCoy.
Here is how to reproduce it:
Starting with the standard "Navigation-Based Application" template in the iPhone OS 2.2.1 SDK:
Open RootViewController.m
Uncomment viewDidLoad, and enable the Edit button:
- (void)viewDidLoad {
[super viewDidLoad];
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
Specify that the table has a few cells:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 4;
}
In tableView:cellForRowAtIndexPath:, add a line to set the text property of a cell, and therefore to use the built-in UILabel subview:
// Set up the cell...
cell.text = #"Test";
To enable reordering, uncomment tableView:moveRowAtIndexPath:toIndexPath:. The default implementation is blank, which is fine in this case since the template doesn't include a data model.
Configure the project for the Simulator, OS 2.2.1, Build and Go. When the app comes up, tap Edit, then slide any row to a new position, tap Done, and then tap each row one at a time. Usually a tap will select a row, turn it blue, and turn its text white. But a tap on the row that you just moved does that and leaves the UILabel's background color as white. The result is a confusing white open space with blue strips on the edges. Oddly enough, after the first bogus tap, another tap appears to correct the problem.
So far I have found a hack that fixes it, but I'm not happy with it. It works by ensuring that the built-in UILabel is non-opaque and that it has no background color, immediately upon selection.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// hacky bugfix: when a row is reordered and then selected, the UILabel displays all crappy
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
for (UIView *view in cell.contentView.subviews) {
if ([[view class] isSubclassOfClass:[UILabel class]]) {
((UILabel *) view).backgroundColor = nil;
view.opaque = NO;
}
}
// regular stuff: only flash the selection, don't leave it blue forever
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
This appears to work, but I don't expect it to be a good idea forever. What is the Right Way to fix this?
This looks like a bug in UITableView's rendering, and you should file a Radar bug report on it. It's like the cells don't get refreshed properly after the move.
One way to work around this for now is to not use the built-in label, but roll your own in the cell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
CGRect frame = cell.contentView.bounds;
frame.origin.x = frame.origin.x + 10.0f;
UILabel *textLabel = [[UILabel alloc] initWithFrame:frame];
[textLabel setAutoresizingMask:UIViewAutoresizingFlexibleRightMargin];
textLabel.tag = 1;
textLabel.textAlignment = UITextAlignmentLeft;
textLabel.backgroundColor = [UIColor clearColor];
textLabel.textColor = [UIColor blackColor];
textLabel.font = [UIFont boldSystemFontOfSize:20.0];
textLabel.numberOfLines = 1;
textLabel.highlightedTextColor = [UIColor whiteColor];
[cell.contentView addSubview:textLabel];
[textLabel release];
}
UILabel *textLabel = (UILabel *)[cell viewWithTag:1];
textLabel.text = #"Test";
return cell;
}
I tried this, and it doesn't exhibit the same sort of white blank rectangle you see with the built-in label. However, adding another non-opaque view to the table cell might not be the best for overall rendering performance.
I don't know how major of a glitch this is, because Apple doesn't want you to persist a selection highlight on a table row (they've been enforcing this lately during the review process). You're supposed to place a checkmark or move on to the next level in the navigation hierarchy with a selection, at which point this white box would only be on the screen for a fraction of a second.
The trick in the solution from Brad appears to be:
textLabel.backgroundColor = [UIColor clearColor];
If you leave the background as the default you still get the problem even when you roll your own cells UITableViewCells.
The reason I left it as the default is because the documentation says it is less computationally costly to use opaque backgrounds. Ideally I wouldn't want to use [UIColor clearColor] to fix this bug.
Maybe a completely custom painted cell would somehow fix it. I haven't tried those before though.
Does anyone else have a solution for this?
Thanks for the info, I was searching how to erase the background color from a UILabel.
I used the following line:
textLabel.backgroundColor = [UIColor clearColor];
and worked perfectly!!!
thanks
Alejandra :)
Selections aren't meant to be shown for extended periods! (We got knocked on this for several of our apps)
??? That means Apple would not approve their own Calendar app on iPhone! When you go to edit the start and end times of the event, the start time is selected indefinitely, it only changes once the user taps to the next field.