I have some issues with a custom UITableViewCell and how to manage things using storyboards. When I put the styling code in initWithCoder: it doesn't work but if I put it in tableView: cellForRowAtIndexPath: it works. In storyboard I have a prototype cell with its class attribute set to my UITableViewCell custom class. Now the code in initWithCoder: does get called.
SimoTableViewCell.m
#implementation SimoTableViewCell
#synthesize mainLabel, subLabel;
-(id) initWithCoder:(NSCoder *)aDecoder {
if ( !(self = [super initWithCoder:aDecoder]) ) return nil;
[self styleCellBackground];
//style the labels
[self.mainLabel styleMainLabel];
[self.subLabel styleSubLabel];
return self;
}
#end
TableViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"NearbyLandmarksCell";
SimoTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
//sets the text of the labels
id<SimoListItem> item = (id<SimoListItem>) [self.places objectAtIndex:[indexPath row]];
cell.mainLabel.text = [item mainString];
cell.subLabel.text = [item subString];
//move the labels so that they are centered horizontally
float mainXPos = (CGRectGetWidth(cell.contentView.frame)/2 - CGRectGetWidth(cell.mainLabel.frame)/2);
float subXPos = (CGRectGetWidth(cell.contentView.frame)/2 - CGRectGetWidth(cell.subLabel.frame)/2);
CGRect mainFrame = cell.mainLabel.frame;
mainFrame.origin.x = mainXPos;
cell.mainLabel.frame = mainFrame;
CGRect subFrame = cell.subLabel.frame;
subFrame.origin.x = subXPos;
cell.subLabel.frame = subFrame;
return cell;
}
I have debugged the code and found that the dequeue... is called first, then it goes into the initWithCoder: and then back to the view controller code. What is strange is that the address of the cell in memory changes between return self; and when it goes back to the controller. And if I move the styling code back to the view controller after dequeue... everything works fine. It's just I don't want to do unnecessary styling when reusing cells.
Cheers
After initWithCoder: is called on the cell, the cell is created and has its properties set. But, the relationships in the XIB (the IBOutlets) on the cell are not yet complete. So when you try to use mainLabel, it's a nil reference.
Move your styling code to the awakeFromNib method instead. This method is called after the cell is both created and fully configured after unpacking the XIB.
Related
While implementing custom UITableViewCell in tableView:cellForRowAtIndexPath: whats the difference between these two ways
loadNibNamed:owner:options:
and
SimpleTableCell *cell = [[SimpleTableCell alloc]init];
does loadNibNamed:owner:options: also alloc init? if not how SimpleTableCell will work without alloc init?
SimpleTableCell *cell = (SimpleTableCell *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"SimpleTableCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
there is no explicit call of SimpleTableCell *cell = [[SimpleTableCell alloc]init];
OK, first off. I'm not actually going to answer the question at all. Instead I'll tell you how to create and use a custom UITableViewCell subclass. What you're doing at the moment isn't right.
Let's stick to the name SimpleTableCell that you have used.
Create the sub class
Create a subclass of UITableViewCell.
SimpleTableCell.h
#interface SimpleTableCell : UITableViewCell
// if coding only
#property (nonatomic, strong) UILabel *simpleLabel
// if from nib
#property (nonatomic, weak) IBOutlet UILabel *simpleLabel;
#end
SimpleTableCell.m
#import "SimpleTableCell.h"
#implementation SimpleTableCell
// if coding only
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
//create the simpleLabel and add to self.contentView
}
return self;
}
// if from nib no need to do anything at all
// other stuff...
- (void)prepareForReuse
{
// empty the cell here.
// means you don't have to empty everything out in the controller
self.simpleLabel.text = #"";
}
#end
OK, so now we have a cell class.
Create the NIB if that's what you want
It looks like you're doing this already.
Create the nib with the same name (or not, doesn't really matter).
Make the top level item a UITableViewCell and set the subclass to SimpleTableCell. Now connect the outlets. In this example simpleLabel is all there is to connect.
Register the subclass with the table view
In the view controller that owns the table view. This means the table view can deal with creating and dequeueing the cells and you don't have to manually create them at all.
- (void)viewDidLoad
{
[super viewDidLoad];
// set up the other stuff...
// if coding only
[self.tableView registerClass:[SimpleTableCell class] forCellReuseIdentifier:#"SimpleCell"];
// if from a nib
UINib *cellNib = [UINib nibWithNibName:#"SimpleTableCell" bundle:[NSBundle mainBundle]];
[self.tableView registerNib:cellNib forCellReuseIdentifier:#"SimpleCell"];
}
Now you just let the table deal with creating the cells. It keeps track of the reuse identifiers and cell queues so it can handle everything as normal. You just need to ask it to dequeue a cell for you with the identifier you registered you subclass for.
- (UITableViewCell*)tableView:(UITableView *)tableView cellFroRowAtIndexPath:(NSIndexPath *)indexPath
{
// This API was introduced in iOS6 and will ALWAYS return a valid cell.
// However, you need to register the class or nib with the table first.
// This is what we did in viewDidLoad.
// If you use a storyboard or nib to create a tableview and cell then this works too.
SimpleTableCell *mySimpleCell = [tableView dequeueReusableCellWithIdentifier:#"SimpleCell" forIndexPath:indexPath];
mySimpleCell.simpleLabel.text = #"Hello, World";
return mySimpleCell;
}
EDIT
You can create a cell (indeed any class) using...
SimpleTableCell *cell = [[SimpleTableCell alloc] init];
But doing it this way means it isn't associated to a table view or part of a queue.
One step down (if you like) from the method in my answer is to use the old dequeuReusableCell... method and then to check if it's nil and create it like this...
- (UITableViewCell*)tableView:(UITableView *)tableView cellFroRowAtIndexPath:(NSIndexPath *)indexPath
{
// Old way, don't do this if you're targeting iOS6.0+
SimpleTableCell *mySimpleCell = [tableView dequeueReusableCellWithIdentifier:#"SimpleCell"];
if (!mySimpleCell) {
// have to use initWithStyle:reuseIdentifier: for the tableView to be able to dequeue
mySimpleCell = [[SimpleTableCell alloc] initWithStyle:UITableViewCellStyleCustom reuseIdentifier:#"SimpleCell"];
}
mySimpleCell.simpleLabel.text = #"Hello, World";
return mySimpleCell;
}
I'm not even sure you can load from a nib in here as you wouldn't be able to set the reuse identifier on the cell.
Multiple cell subclasses
OK, last edit :D
For multiple UITableViewCell subclasses you can use this too. I have done exactly this in the past. You might, for instance, have a cell for a Post item a cell for an Image item a cell for a Comment item etc... and they are all different.
So...
- (void)viewDidLoad
{
// the rest
// register each subclass with a different identifier
[self.tableView registerClass:[PostCell class] forCellReuseIdentifier:#"PostCell"];
[self.tableView registerClass:[ImageCell class] forCellReuseIdentifier:#"ImageCell"];
[self.tableView registerClass:[CommentCell class] forCellReuseIdentifier:#"CommentCell"];
}
To help keep this small (and it comes in handy for NSFetchedResultsControllers too, I move the configuration of the cell out to another method.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell;
if (the required cell is a post cell) {
cell = [tableView dequeueReusableCellWithIdentifier:#"PostCell" forIndexPath:indexPath];
[self configurePostCell:(PostCell *)cell atIndexPath:indexPath];
} else if (the required cell is a image cell) {
cell = [tableView dequeueReusableCellWithIdentifier:#"ImageCell" forIndexPath:indexPath];
[self configureImageCell:(ImageCell *)cell atIndexPath:indexPath];
} else if (the required cell is a comment cell) {
cell = [tableView dequeueReusableCellWithIdentifier:#"CommentCell" forIndexPath:indexPath];
[self configureCommentCell:(CommentCell *)cell atIndexPath:indexPath];
}
return cell;
}
- (void)configurePostCell:(PostCell *)postCell atIndexPath:(NSIndexPath *)indexPath
{
// get the object to be displayed...
postCell.postLabel = #"This is the post text";
postCell.dateLabel = #"5 minutes ago";
}
- (void)configureImageCell:(ImageCell *)imageCell atIndexPath:(NSIndexPath *)indexPath
{
// get the object to be displayed...
imageCell.theImageView.image = //the image
imageCell.dateLabel = #"5 minutes ago";
}
- (void)configureCommentCell:(CommentCell *)commentCell atIndexPath:(NSIndexPath *)indexPath
{
// you get the picture...
}
loadNibNmed:... is asking the system to recreate an object (usually, but not limited to a UIView) from a canned instance in a nib file. The nib file is created using the interface builder portion of Xcode.
When an object is loaded from a nib file (loadNibNamed...) init doesn't get called, instead initWithCoder: gets called. If you're looking to do post initialization setup on a view loaded from a nib file, the usual method is to awakeFromNib and call [super awakeFromNib]
I'm doing my project with storyboards and I'm trying to implement a custom UITableViewCell.
I would like to do the following in my custom cell:
#import "CustomCell.h"
#implementation CustomCell
#synthesize myLabel, myButton;
- (instancetype)initWithCoder:(NSCoder *)decoder
{
if ((self = [super initWithCoder:decoder]))
{
//want to custom setup of properties placed in the cell
self.myButton.backgroundColor = [UIColor redColor];//this does NOT work
//and so forth...
}
return self;
}
But the background color is not set. It only works when I set the background color of the button in the tableViewController's cellForRowAtIndexPath function, like this
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier:#"CustomCell"];
MyObject *obj = self.myObjects[indexPath.row];
cell.myButton.backgroundColor = [UIColor redColor];//This works!
cell.myLabel.text = obj.name;
return cell;
}
And I have trying debugging by setting break points and NSLog and the initWithCoder gets called before cellForRowAtIndexPath for every cell??
But the background color of the button in the cell does not show when I set it in the custom cell.
Can any help?
Try using the awakeFromNib method instead of initWithCoder: to do any initial customization of the cell. And as mentioned in comments, for simple things like background colors of controls, you can probably just do that in Xcode via the storyboard.
I have a UITableView tall enough that it necessitates scrolling. The top-most cell in the table contains a UITextField for the user to enter some text.
The standard way to build this might be to create and add the text field and add it to a cell created or recycled in cellFOrRowAtIndexPath: However, this constant re-creation means that the text entered in the field is erased when the cell is scrolled out and back into view.
The solutions I've found so far suggest using UITextField delegation to track the text as it changes and store it in an iVar or property. I would like to know why this is recommended instead of the simpler approach I am using:
I am creating the UITextField in the init method of the UITableViewController and immediately storing it in a property. In cellFOrROwAtIndexPath I am simply adding the pre-existing field instead of initializing a new one. The cell itself can be recycled without issue, but because I am always using the one and only UITextField, the content is maintained.
Is this a reasonable approach? What might go wrong? Any improvements (perhaps I could still create the field in cellForRowAtIndexPath but first check if the property is nil?)
When you are creating cells in cellForRowAtIndexPath you have to use one reusable identifier for that first cell (ie. cellId1) and another for the rest (ie. cellId2).
If you do this, when you get the cell for the first element by calling [tableView dequeueReusableCellWithIdentifier:#"cellId1"] you will always get the same Object and will not be reused by other cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = nil;
// Only for first row
if (indexPath.row == 0) {
static NSString *cellId1 = #"cellId1";
cell = [tableView dequeueReusableCellWithIdentifier:cellId1];
if (cell == nil) {
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId1];
}
}
else {
static NSString *cellId2 = #"cellId2";
cell = [tableView cellId2];
if (cell == nil) {
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault cellId2];
}
}
// do whatever
return cell;
}
If there is only one UITextField, then I agree that your approach would be better/same as compared to using UITextField delegation (I think).
However, let us assume that you want to "expand" your view so that there are about 7-8 or more TextFields now. Then if you go about using your approach, then the problem will be that you will be storing 7-8 or more TextFields in memory and maintaining them.
In such a situation, a better approach would be that you create only that number of textfields as visible on screen. Then you create a dictionary which would maintain the content present in the textfield (which you can get by UITextFieldDelegate methods). This way, the same textfield can be used when the cell is reused. Only the values will change and will be dictated by the values in the dictionary.
On a sidenote, do minimal creation in cellForRowAtIndexPath as that is called during every table scroll and so creating a textField in cellForRowAtIndexPath can be expensive.
#import "ViewController.h"
#import "TxtFieldCell.h"
#define NUMBER_OF_ROWS 26
#interface ViewController ()<UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>
#property (weak, nonatomic) IBOutlet UITableView *tablView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tablView.datasource = self; //set textfield delegate in storyboard
textFieldValuesArray = [[NSMutableArray alloc] init];
for(int i=0; i<NUMBER_OF_ROWS; i++){
[textFieldValuesArray addObject:#""];
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
#pragma mark - TableView Datasource
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TxtFieldCell *cell = [tableView dequeueReusableCellWithIdentifier:#"TxtFieldCellId" forIndexPath:indexPath];
cell.txtField.tag = indexPath.row;
if (textFieldValuesArray.count > 0) {
NSString *strText = [textFieldValuesArray objectAtIndex:indexPath.row];
cell.txtField.text = strText;
}
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return NUMBER_OF_ROWS;
}
#pragma mark - TextField Delegate
- (void)textFieldDidEndEditing:(UITextField *)textField {
[textFieldValuesArray replaceObjectAtIndex:textField.tag withObject:textField.text];
}
This is working fine for my plain style table views, but not for my grouped style. I'm trying to customize how the cell looks when it is selected.
Here is my code:
+ (void)customizeBackgroundForSelectedCell:(UITableViewCell *)cell {
UIImage *image = [UIImage imageNamed:#"ipad-list-item-selected.png"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
cell.selectedBackgroundView = imageView;
}
I have verified that the correct cell is indeed being passed into this function. What do I need to do differently to make this work?
It's not clear from your question whether or not you're aware that the tableViewCell automatically manages showing/hiding it's selectedBackgroundView based on its selection state. There are much better places to put that method other than in viewWillAppear. One would be at the time you initially create the tableViewCells, i.e.:
- (UITableViewCell *)tableView:(UITV*)tv cellForRowAtIP:(NSIndexPath *)indexPath {
UITableViewCell *cell = nil;
cell = [tv dequeueCellWithIdentifier:#"SomeIdentifier"];
if (cell == nil) {
cell = /* alloc init the cell with the right reuse identifier*/;
[SomeClass customizeBackgroundForSelectedCell:cell];
}
return cell;
}
You only need to set the selectedBackgroundView property once in the lifetime of that cell. The cell will manage showing/hiding it when appropriate.
Another, cleaner, technique is to subclass UITableViewCell, and in the .m file for your subclass, override:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithBla....];
if (self) {
UIImageView *selectedBGImageView = /* create your selected image view */;
self.selectedBackgroundView = selectedBGImageView;
}
return self;
}
From then on out your cell should show it's custom selected background without any further modifications. It just works.
Furthermore, this method works better with the current recommended practice of registering table view cell classes with the table view in viewDidLoad: using the following UITableView method:
- (void)registerClass:(Class)cellClass forCellReuseIdentifier:(NSString *)identifier
You would use this method in your table view controller's viewDidLoad method, so that your table view cell dequeuing implementation is much shorter and easier to read:
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[SomeClass class]
forCellReuseIdentifier:#"Blah"];
}
- (UITableViewCell *)tableView:(UITV*)tv cellForRowAtIP:(NSIndexPath *)indexPath {
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:#"Blah"
forIndexPath:indexPath];
/* set your cell properties */
return cell;
}
This method is guaranteed to return a cell as long as you have registered a class with the #"Blah" identifier.
I am trying to create a "settings" table view for my app. I am trying to mimic it to be the same style as the gneral setting on an Iphone. I have created my own custom cell class by inheriting from UITableCell. I gave it the appropriate IBOulets and i have hooked them up in the storyboard. I also hooked up the switch to my tableViewControler, but for some reason my code is only returning me one empty cell (it being only one cell is not an issue atm for that's all i have in my setting). I triple checked and made sure that I'm using the same cell identifier in my code and in storyboard. Anyone know why I'm getting a blank cell back?
Here is my .h file for my custom cell.
#interface NHPSettingsCell : UITableViewCell
#property (nonatomic,weak) IBOutlet UILabel *settingLabel;
#property (nonatomic,strong) IBOutlet UISwitch *settingSwitch;
#end
MY Problem code is here, my .h file for the custom cell:
#import "NHPSettingsCell.h"
#implementation NHPSettingsCell
#synthesize settingLabel, settingSwitch;
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
}
return self;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
#end
My method for drawing the cell in my custom view controller:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"SettingsCell";
NHPSettingsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[NHPSettingsCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.selectionStyle = UITableViewCellStyleDefault;
}
cell.settingLabel.text = #"firstSetting";
//check the if user wants promt of disclaimer each time or not.
if([prefs boolForKey:#"firstSetting"] == YES){
cell.settingSwitch.on = YES;
}else{
cell.settingSwitch.on = NO;
}
return cell;
}
Now the thing that annoys me is i have successfully managed to implement the cellForRowAtIndexPath method for a dynamic table that uses custom cells. I have also implements the code for a static table using the default cell, but for a static table with custom cells it just doesn't seem to work. Here is the code on how I implemented my custom cells on a dynamic table (note how i didn't have to init the cells but it works).
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"InteractionResultCell";
NHPResultCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure & Fill the cell
cell.leftLabel.text = [[resultsList objectAtIndex:indexPath.row] substanceName];
cell.rightLabel.text = [[resultsList objectAtIndex:indexPath.row] substanceName2];
NSString *color = [NSString stringWithFormat:#"%#", [[resultsList objectAtIndex:indexPath.row] color]];
//Change a hex value to a readable 0x number to pass ot hte macro so we can go from a hex color to a RGB system.
NSScanner *scanner;
unsigned int tempint=0;
scanner = [NSScanner scannerWithString:color];
[scanner scanHexInt:&tempint];
cell.severityButton.backgroundColor = UIColorFromRGB(tempint);
return cell;
}
Two problems:
If you are using static cells, do not implement any datasource methods in your view controller (numberOfRows, numberOfSections, cellForRow...) as this will override what you have built in the storyboard. The table has the sections, rows and content you give it in the storyboard.
Cells loaded from the storyboard (either dynamic prototypes, or static cells) are initialised using initWithCoder:, not initWithStyle:. awakeFromNib: is a better place to put your set up code.
dequeueReusableCellWithIdentifier: works only if a cell has already been created to prevent repeated memory allocations. You cannot reuse a cell without creating it first. The static cells created in the xib are the default type. That's why it doesn't work for static table with custom cells. Add the cell creation code after reuse as you've done in your custom view controller's cellForRowAtIndexPath: method:
if (cell == nil) {
cell = [[NHPSettingsCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.selectionStyle = UITableViewCellStyleDefault;
}
EDIT- To init your custom cell, you'll have to load from xib. Add the following class method to your NHPSettingsCell.m:
+(NHPSettingsCell*) createTextRowWithOwner:(NSObject*)owner{
NSArray* wired = [[NSBundle mainBundle] loadNibNamed:#"NHPSettingsCell" owner:owner options:nil];
NHPSettingsCell* cell = (NHPSettingsCell*)[wired firstObjectWithClass:[NHPSettingsCell class]];
return cell;
}
and then call it from your custom view controller as:
cell = (NHPSettingsCell*)[tableView dequeueReusableCellWithIdentifier: CellIdentifier];
if (Nil == cell) {
cell = [NHPSettingsCell createTextRowWithOwner:self];
}