I've created my own CustomTableView and CustomCell. The cell is in a xib, and I'm registering it when initializing the tableView, like this:
[self registerNib:[UINib nibWithNibName:#"CustomCell" bundle:nil]
forCellReuseIdentifier:kCustomCellIdentifier];
If I don't do this, I won't be able to define what ReuseIdentifier should "point" to this class. This is my cellForRow:
-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomCell *cell = [self dequeueReusableCellWithIdentifier:
kCustomCellIdentifier forIndexPath:indexPath];
if(!cell)
{
cell = [[[NSBundle mainBundle] loadNibNamed:#"CustomCell"
owner:self options:nil] objectAtIndex:0];
cell.delegate = self;
[cell initialSetup]; //Other stuff
}
else
[cell resetCell];
//And other awesome stuff
return cell;
}
This 'works'. When I lauch my app, my own custom cells are showing.
However, it turns out the cell is NEVER returned as nil from [self dequeue... Thus, the if-statement if(!cell) is never true. I have additional setup inside this statement that I want to perform, but I don't know where the cell's are being initialized the first time now. If I remove registerNib, then this statement is true, but then it's true for all cells, and none will ever be dequeued.
I can probably work around this, and put my initialSetup (and other stuff) inside the -(id)initWithCoder..-method in the CustomCell-class, but I'd like to know where my cells are being initialized right now. Why do my cells exist before cellForRowAtIndexPath?
When you register a class or a nib in a table view using method registerClass:forCellReuseIdentifieror registerNib:forCellReuseIdentifier the tableview internally will create an instance of the cell if no one is available when you call dequeueReusableCellWithIdentifier:, so initialization code is no longer needed inside the delegate.
From the UITableView.h code:
// 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);
Depending of the which register method is used the init methods called are:
initWithStyle:reuseIdentifier for cells registered using registerClass:
initWithCoder: for cells registered using registerNib:
If you are using registerNib: you can use too awakeFromNib method in the cell, that is also a good place to put initialization code of the cell. The main difference between using initWithCoder: or awakeFromNib its explained in this question.
When a cell is reused, you have the method prepareForReuse in the cell to make some cleanup in the cell and left it prepared to be configured again.
A good approach to work with all of this will be:
//ViewController code
- (void)viewDidLoad
{
...
[_tableView registerNib:[UINib nibWithNibName:#"CellSample" bundle:Nil] forCellReuseIdentifier:#"cellIdentifier"];
...
}
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"cellIdentifier"];
//configure the new cell, no if (!cell) needed
return cell;
}
//Cell code
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
//You can put initialization here
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
//But is better initialize here
}
- (void)prepareForReuse
{
//Reuse and reset the cell here
}
Hope it helps
When using dequeueReusableCellWithIdentifier: forIndexPath: you don't have to alloc the cell like you would if you were using default UITableViewCell. In your custom UITableViewCell subclass, this is called when it is initialized:
- (void)awakeFromNib {
[super awakeFromNib];
}
So add that in there and you should be good.
- (instancetype)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
//custom your cell
}
return self;
}
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 have custom UiTablleviewCell with some images and labels, and I would like to have rotated label in tableview cell...so I would like to edit initWithStyle method, but it seems like it's never called.
- (id)initWithStyle:(UITableViewCellStyle)stylereuseIdentifier:(NSString*)reuseIdentifier{
NSLog(#"creating cell");
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self)
{
}
return self;}
but in my log, I cant see this message. In tableview I have standard cellForRow method
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"messagesCell";
TBCellMessagesCell *cell = (TBCellMessagesCell *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
// smt stuff
return cell;
}
so I'm wondering how does tableview initialize tableviewcells, I can think about some workarounds but I would like to have it clean.
Thank you.
If the cells come from a storyboard or nib file, then initWithStyle:reuseIdentifier is not called, initWithCoder: is called instead.
Here's a typical implementation of an overwritten initWithCoder::
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Do your custom initialization here
}
return self;
}
Will not work if you need to access IBOutlet during custom initialization.
You might also consider the awakeFromNib method if you need to talk with IBOutlet.
Discussion
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.
I'm using Xcode 5, and want to be using the recommended best practices from Apple, which includes dynamic prototype cells and using registerClass:forCellReuseIdentifier.
I have created a storyboard and dropped a UITableView with 1 prototype dynamic cell on it. I've set the class of the cell to ItemCell, and set the reuse identifier to ItemCell.
The ItemCell class contains a nameLabel IBOutlet, which I've connected to the label within the prototype cell by dragging.
In the ViewController, I register the ItemCell class to be used for the ItemCell reuse identifier:
- (void)viewDidLoad {
[super viewDidLoad];
[_tableView registerClass:[ItemCell class] forCellReuseIdentifier:#"ItemCell"];
}
In tableView:cellForRowAtIndexPath, I dequeue the cell and set the properties for the nameLabel. self.items is an NSArray of strings.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ItemCell *cell = [tableView dequeueReusableCellWithIdentifier:#"ItemCell" forIndexPath:indexPath];
cell.nameLabel.text = [self.items objectAtIndex:indexPath.row];
return cell;
}
So: it's being created as an ItemCell, but it's not loading it from the storyboard. I've confirmed this by overriding initWithStyle:reuseIdentifier and initWithCoder, to see which was being called:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
NSLog(#"NOT STORYBOARD");
}
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
// Initialization code
NSLog(#"Yay, it's working!");
}
return self;
}
Every time, it's initWithStyle that's being called. From everything I've read, this should be working. I can't find anything that indicates I need to somehow register the class differently when it's in a storyboard, but clearly the cell isn't aware that it's got a storyboard associated with it.
I'm sure I'm making a total newbie mistake, but I can't figure it out. Any help would be appreciated.
You don't have to (and should not) call registerClass for prototype cells defined in the storyboard. initWithCoder is called automatically if a prototype cell has to
be instantiated from the storyboard.
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.
I have a UICollectionView that contains custom UICollectionViewCells (TestReceiptCell is the class name).
I was not having any problems getting the UICollectionView to appear and load the custom cells when the custom cells only contained a UILabel.
I then added a UITableView via IB into the TestReceiptCell NIB file. I set a referencing outlet in TestReceiptCell.h for the UITableView and synthesized in the .m file. I set the delegate and datasource for the UITableView to the ViewController containing the UICollectionView.
Now when running the app I get a EXC_ BAD_ ACCESS exception in this block on the third line:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"TestReceiptCell";
TestReceiptCell *cell = (TestReceiptCell *)[collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath]; //exception thrown here
return cell;
}
I ran the Zombie Instrument test and found that the deallocated memory call originates here. This is my first time using that instrument so I am not exactly sure how to investigate from here.
For reference, here are some more relevant parts of the code:
ViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[self.myCollectionView registerNib:[UINib nibWithNibName:#"TestReceiptCell" bundle:nil] forCellWithReuseIdentifier:#"TestReceiptCell"];
// Setup flowlayout
myCollectionViewFlowLayout = [[UICollectionViewFlowLayout alloc] init];
[myCollectionViewFlowLayout setItemSize:CGSizeMake(310, 410)];
[myCollectionViewFlowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
[self.myCollectionView setCollectionViewLayout:myCollectionViewFlowLayout];
self.myCollectionView.pagingEnabled = YES;
}
I am implementing the UITableView datasource and delegate methods in the ViewController.m file as well but I am not sure if the problem lies here given the origination of the EXC_BAD_ACCESS exception:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:#"eventCell"];
if(!cell){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"eventCell"];
}
return cell;
}
UPDATE:
I am able to get this to run if I change cellForItemAtIndexPath to:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"TestReceiptCell";
//TestReceiptCell *cell = (TestReceiptCell *)[collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
TestReceiptCell *cell = [NSBundle.mainBundle loadNibNamed:#"TestReceiptCell" owner:self options:nil][0];
return cell;
}
However, I am not dequeuing cells and know this is not the correct way. There seems to be an issue somewhere in the initWithFrame method that gets called when dequeueReusableCellWithResueIdentifier creates a new cell. Here is that method currently:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:#"TestReceiptCell" owner:self options:nil];
if ([arrayOfViews count] < 1) {
return nil;
}
if (![[arrayOfViews objectAtIndex:0] isKindOfClass:[UICollectionViewCell class]]) {
return nil;
}
self = [arrayOfViews objectAtIndex:0];
}
return self;
}
EDIT:
If I do not select a delegate or a datasource for the tableview, the collectionview with tableviews will load. Something in attaching the delegate/datasource to File's Owner is causing the error.
When you register a UINib for cell reuse, dequeueReusableCellWithReuseIdentifier:forIndexPath: is what calls instantiateWithOwner:options: on the UINib that you registered. Whatever it passes for owner, is what becomes the File's Owner outlet in your nib.
It appears that you are expecting the File's Owner to be the UICollectionView, but I don't think that it is.
Even if it were, I don't think you should use the UICollectionView for the delegate of the UITableView contained within each collection cell. That would require your UICollectionView to keep track of the tableViews and contents within each cell.
I'd suggest setting the delegate of the contained tableView to the collection cell itself and have each cell manage its own tableview.
EDIT:
You can define a delegate protocol for your collection view cells to communicate the relevant table view events to the collection view. With this approach, you would set the delegate property you define for each collection cell in the collectionView:cellForItemAtIndexPath method of your collection view datasource.
When the user, for example, selects an item from the table, you can call the cell delegate to inform the collection view which item was selected.
This approach allows you to abstract the fact that your collection cell is using a table view to display the cell information. Later, if you decide you want to use, for example, an embedded UICollectionView to display those items, the delegate protocol can remain unchanged and you can isolate your changes to the collection cell.