I'm trying to clean up my code and use MVC principles by pushing as much view related stuff as i can into the storyboard as well as custom UIView classes (ie as opposed to doing view related stuff on the UIViewController itself)
So I have a custom UITableViewCell (called CustomCell) that has several properties, one of them is my own label. Since I'm loading the cell from the storyboard, I initialize it with initWithCoder rather than initWithStyle:reuseIdentifier:, this is what I have in CustomCell.m which is a subclass of UITableViewCell (for some reason i couldn't figure out how to set a custom font using storyboard.. but that's beside the point of this question):
// CustomCell.m - subclass of UITableViewCell
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
NSLog(#"customizing cell font having text %#", self.label.text);
UIFont *customFont = [UIFont fontWithName:#"Montserrat" size:16];
self.label.textColor = [UIColor redColor];
[self.label setFont:customFont];
}
return self;
}
This simply doesn't work.. the log statement outputs null for the text simply b/c the text hasn't been loaded yet. self.label is also null (I don't know why I thought it should have been inflated from the nib by now) but even if I initialize it here.. it still won't work.
So my work around was to simply put this cell customization part here in the TableViewController:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath {
UIFont *customFont = [UIFont fontWithName:#"Montserrat" size:16];
[((CustomCell *)cell).label setFont:customFont];
}
and it worked just fine.. I'm unhappy with this method and I would like to know how to make it work from within CustomCell.m
update: to make things even more interesting.. if i put customization code for UITableViewCell properties inside initWithCoder, they work! consider this example:
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
UIView *bgColorView = [[UIView alloc] init];
[bgColorView setBackgroundColor:[UIColor blueColor]];
[self setSelectedBackgroundView:bgColorView]; // actually works!
}
return self;
}
which makes this even more weird.
Inspired by andykkt's comment, I found the explanation in awakeFromNib documentation:
The nib-loading infrastructure sends an awakeFromNib message to each
object recreated from a nib archive, but only after all the objects in
the archive have been loaded and initialized. When an object receives
an awakeFromNib message, it is guaranteed to have all its outlet and
action connections already established.
so that explains the weird behaviour above: initWithCoder instantiates a standard UITableViewCell (that already comes pre-baked with a backgroundView etc etc).. however it still doesn't recognize the outlets I've added to it via storyboard.. awakeFromNib does.
I suspect that initWithCoder: method doesn't draw the contents of the cell, it just, initialises the object, and as you have noticed, accessing any of the ui drawing methods of the cell actually 'draws' the cell to apply the changes you have made to the cell. You can see the same behaviour when initing UIViewController's using initWithNibName:bundle: method.
Related
I have a static UITableView setup in my storyboard. I have subclassed some cells and added this property:
#property (nonatomic, strong) UILabel *label;
I also have code to add the label to my cell in the layoutSubviews method. My label shows in my cell without an issue. I can see the label saying 'PLACEHOLDER' but it is not updated to say the text I set in cellForRowAtIndexPath.
In the cellForRowAtIndexPath method, I have the following:
APInfoTableViewCell *cell = (APInfoTableViewCell*)[super tableView:tableView cellForRowAtIndexPath:indexPath];
[cell.label setText#"Hello World"];
However, this will not update the text in my label. The label is actually nil.
EDIT:
Here is the full code...
UITableViewCell Subclass:
#property (nonatomic, strong) UILabel *infoLabel;
-(void)layoutSubviews
{
[super layoutSubviews];
[self initUI];
}
-(void)initUI
{
//add long press to show info label
_infoLabel = [UILabel new];
[_infoLabel setText:#"PLACEHOLDER"];
[_infoLabel setFont:[UIFont fontWithName:#"Avenir-Medium" size:10.0f]];
[_infoLabel setTextAlignment:NSTextAlignmentCenter];
[_infoLabel setTextColor:[UIColor apText]];
[_infoLabel setAlpha:1.0f];
[_infoLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:_infoLabel];
NSArray *infoConstraintH = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-40-[infoLbl]-40-|" options:0 metrics:nil views:#{ #"infoLbl" : _infoLabel }];
[self addConstraints:infoConstraintH];
NSArray *infoConstraintV = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-10-[infoLbl]-10-|" options:0 metrics:nil views:#{ #"infoLbl" : _infoLabel }];
[self addConstraints:infoConstraintV];
}
ViewController:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
APInfoTableViewCell *cell = (APInfoTableViewCell*)[super tableView:tableView cellForRowAtIndexPath:indexPath];
[cell.infoLabel setText#"Hello World"];
return cell;
}
If you are using storyboard, and you don't want to add IB outlet then call your initUI method from awakeFromNib method.
If you are not using storyboard implement - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier method and call it from there.
awakeFromNib method is called once the cell(or any UIView subclass) is instantiated from nib(either from storyboard or from xib). And its called only once in the lifetime of the view
Note: This will not get called if you are creating a view subclass programmatically.
As per documentation
Subclasses can override this method as needed to perform more precise layout of their subviews. You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly.
That means, use this method to do any additional lay outing of already added. This method is called when some frame/autolayout constraint changes happens.
You need to instantiate a new cell in tableView:cellForRowAtIndexPath: or use dequeueReusableCellWithIdentifier:. cellForRowAtIndexPath: returns visible instantiated cells in the table view. As you aren't instantiating your cells anywhere there are no cells in the table view, so cellForRowAtIndexPath: returns nil.
Additionally layoutSubviews is called a lot and is called late. You should not rely on it to initialise your label. As it stands your cell will keep adding additional labels when ever layoutSubviews is called! The designated initialiser is the place to add that initUI code (as the name implies 😉). If your UITableViewCell subclass doesn't use a XIB/Storyboard for its view, initWithStyle:reuseIdentifier: is the designated initialiser. (Otherwise it is initWithCoder: or awakeFromNib)
I'm struggling with a problem I encountered while trying to create a custom UITableViewCell.
I subclassed UITableViewCell in SGTableViewCell and added it in the prototype cell in the storyboard.
As you can see the label is connected
and the cell identifier is set correctly
Then I linked the label to the SGTableViewCell.h like this
#property (strong, nonatomic) IBOutlet UILabel *nameLabel;
and in the .m file I have this code
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
[self addGestureRecognizer:recognizer];
self.nameLabel = [[UILabel alloc] init];
_checkView = [[UIView alloc] initWithFrame:CGRectNull];
_checkView.backgroundColor = kGreen;
_checkView.alpha = 0.0;
[self addSubview:_checkView];
self.nameLabel.text = #"Hello";
}
return self;
}
But when I use this cell in my tableview using this code
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
Episode *episode = [self.selectedSeason.episodeList objectAtIndex:indexPath.row];
SGTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Episode"];
UIView *selectionColor = [[UIView alloc] init];
selectionColor.backgroundColor = kSelectionGrey;
cell.selectedBackgroundView = selectionColor;
cell.backgroundColor = kBackgroundGrey;
cell.nameLabel.text = episode.name;
NSLog(#"%#", cell.nameLabel.text);
cell.nameLabel.textColor = [UIColor redColor];
return cell;
}
I get no text at all.
I tried logging the text from each label in each cell and it gives me the right text.
I tried setting programmatically a different disclosure indicator for the custom cell and it did change so everything is allocated and working but label is not displaying.
I honestly have no idea of what's the problem. Did I miss something?
Thank you
PARTIALLY SOLVED:
OK i tried doing the same thing on an empty project and everything worked flawlessly so I checked again my project and found this line
[self.tableView registerClass:[SGTableViewCell class] forCellReuseIdentifier:#"Episode"];
Seeing it was not necessary for the empty project i commented this line and everything started working.
The only problem i have now is that if i don't use this line i can't use the custom cell as was intended. In fact my custom cell is swipable using a pan gesture recognizer but without registering my custom class to the tableview seems like the swipe doesn't work.
Sorry for the trouble, seems like i messed up again :/
You shouldn't alloc init a label that you created in the storyboard, it is already allocated automatically. When you do self.nameLabel = [[UILabel alloc] init];, you reset the self.nameLabel property to point to a new empty memory location and not to the label created in the storyboard, hence you can change its text property and see the result in NSLog but not in the storyboard because it doesn't refer to that label in the storyboard.
Try removing all initialisation from the initWithStyle method (to make sure nothing is covering it such as that subview you create), and everything related to the label in the cellForRowAtIndexPath method (same reason), and try a simple assignment like self.nameLabel.text = #"Test text" in the cellForRowAtIndexPath method, it should work. Then add all your other initialisation.
And yeah, don't forget to input your cell reuse identifier "Episode" in the storyboard.
Make sure you:
Have linked the delegate and the datasource to the view the tablview is housed in.
Have that view implement UITableViewController and UITableViewDelegate (I'm pretty sure it is both of those).
Implement the necessary methods, which you seem to have done. You need the row size, section size, and the add cell methods
After updating the array linked to your tableview, call [tableView reloadData]
Have a look at this link:Tutorial to create a simple tableview app
Earlier I was creating custom cells without prototype, just custom class, register that class for reuse identifier with my UITableView and use like that.
In that case, I used: - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier and that worked great (except for UITableViewHeaderFooterView, they never knew their frame even though that method was called, frame was 0 0 0 0 - but that's not my question here).
Now I was creating some custom UITableViewCells using Interface Builder. Created their custom class, connected outlets from Interface Builder to my custom class and I'v noticed that - (id)initWithCoder:(NSCoder *)aDecoder is being called and that's where I need to do my modifications.
BUT, I wasn't able. That's the only init method being called, but if I try to change font of my label there, it wont be changed. I also tried to round rect button but that is also not working (my button is also having frame 0 0 0 0 - not yet existing) in that method.
My question is, where should I modify things like font, background color of elements created using Prototype Cells?
(id)initWithCoder:(NSCoder *)aDecoder is the correct initializer.
Try this:
#implementation TESTCell
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
self.textLabel.font = [UIFont boldSystemFontOfSize:25.0f];
}
return self;
}
#end
Other customizations (of own subviews) goes to:
- (void) awakeFromNib
{
[self.myButton setTitle:#"Hurray" forState:UIControlStateNormal];
}
It works fine. Of course, you have to set the identifier of the cell in your StoryBoard or Interface Builder file to the same identifier you use in your UITableViewController.
I use this piece of code in my UITableViewController:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"TESTCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.textLabel.text = #"TEST";
return cell;
}
I am reading the apple docs on setting up custom subclasses of UITableViewCell - Docs
In this example I need to setup a custom cell which does not have a NIB/storyboard file. The apple docs provide an example of using a predefined style and configuring that but not creating a completely custom layout.
How should the cell be called in.. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath ?
I am looking to have a completely custom layout so is this correct? As the cell is being called initWithStyle...?
MESLeftMenuCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if(cell == nil) {
cell = [[MESLeftMenuCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
In the custom cell subclass how/where should I implement the setup of the views within the contentView?
Which method is called for the init, would it be initWithStyle as above? If so, can I simply create the cell outlets in there once only?
Then in the cellForRowAtIndexPath can I access the outlets as i.e. cell.MainLabel.text ... ?
This is how I have been shown to set up my Collection View Cells in their custom class. I know you are using a tableview but this is threw me for a while so decided to add here. Hopefully it helps you.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (id)initWithCoder:(NSCoder *)encoder
{
self = [super initWithCoder:encoder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit
{
// set up your instance
}
To access the outlets of that cell I just add outlets to the header file of the custom class and you can easily access them.
As of iOS 6, you no longer need to check whether the dequeued cell is nil.
What you would do is as follows:
In the viewDidLoad method of the view controller containing the table view you could say
[self.tableView registerClass:[MyCellClass class] forCellReuseIdentifier:MyCellIdentifier];
This results in your dequeueReusablecellWithIdentifier call to never return nil. In essence, in the background, initWithStyle is called. So you would set your stuff up when overriding that function.
Here is what I have done:
I created a custom xib file that has a small UIView used for a custom table section header.
I classed the custom xib file.
I want to add this to a tableView as the header. I have looked at a few resources, but they seem to be either outdated or missing information.
Looking in the documentation, I see a reference to adding a custom header with the following instructions:
To make the table view aware of your header or footer view, you need to register it. You do this using the registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: method of UITableView.
When I added a tableView to my storyboard view, it was easy to assign it a reuse identifier within XCode. I was even able to create a custom cell xib file and it also had a spot for a reuse identifier within XCode.
When I created the custom UIView for the section header, it did not have an entry for reuse identifier. Without this, I have no idea how to use registerNib:forCellReuseIdentifier.
More information:
I have a storyboard scene that has a tableView inside. The tableView is of a custom class that is linked and the tableView object has an outlet in the parent view's ViewController file.
The parent ViewController is both the UITableViewDataSourceDelegate and UITableViewDelegate. Again, I was able to implement the custom cells with no issue. I can't even modify the header in any way besides the title.
I tried calling the method [[self tableHeaderView] setBackgroundColor:[UIColor clearColor]]; from the custom tableView class and nothing happens. I tried using this method in the parent ViewController class by using the outlet name like so:
[[self.tableOutlet tableHeaderView] setBackgroundColor:[UIColor clearColor]];
Any help would be greatly appreciated.
EDIT: (Can't change background to transparent)
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
HeaderView *headerView = [self.TableView dequeueReusableHeaderFooterViewWithIdentifier:#"tableHeader"];
// Set Background color
[[headerView contentView] setBackgroundColor:[UIColor clearColor]];
// Set Text
headerView.headerLabel.text = [self.sectionArray objectAtIndex:section];
return headerView;
}
You don't need to set the identifier in the xib -- you just need to use the same identifier when you register, and when you dequeue the header view. In the viewDidLoad method, I registered the view like this:
[self.tableView registerNib:[UINib nibWithNibName:#"Header1" bundle:nil] forHeaderFooterViewReuseIdentifier:#"header1"];
Then, in the delegate methods:
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *headerView = [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:#"header1"];
return headerView;
}
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 100;
}
On the problem with background color (Unless you want transparent):
You can create an UIView which occupies the whole view then change the background color of that.
If you don't want others to know what's happening, you can overwrite the backgroundColor property:
//interface
#property (copy, nonatomic) UIColor *backgroundColor;
//implementation
#dynamic backgroundColor;
- (void)setBackgroundColor:(UIColor *)backgroundColor {
//self.viewBackground is the created view
[self.viewBackground setBackgroundColor:backgroundColor];
}
- (UIColor *)backgroundColor {
return self.viewBackground.backgroundColor;
}