I'm trying to use storyboards in an app I'm working on. In the app there are Lists and Users and each contains a collection of the other (members of a list, lists owned by a user). So, accordingly, I have ListCell
and UserCell
classes. The goal is to have those be re-usable throughout the app (ie, in any of my tableview controllers).
That's where I'm running into a problem.
How do I create a custom tableview cell in the storyboard that can be re-used in any view controller?
Here are the specific things I've tried so far.
In Controller #1, added a prototype cell, set the class to my UITableViewCell
subclass, set the reuse id, added the labels and wired them to the class's outlets. In Controller #2, added an empty prototype cell, set it to the same class and reuse id as before. When it runs, the labels never appear when the cells are shown in Controller #2. Works fine in Controller #1.
Designed each cell type in a different NIB and wired up to the appropriate cell class. In storyboard, added an empty prototype cell and set its class and reuse id to refer to my cell class. In controllers' viewDidLoad
methods, registered those NIB files for the reuse id. When shown, cells in both controllers were empty like the prototype.
Kept prototypes in both controllers empty and set class and reuse id to my cell class. Constructed the cells' UI entirely in code. Cells work perfectly in all controllers.
In the second case I suspect that the prototype is always overriding the NIB and if I killed the prototype cells, registering my NIB for the reuse id would work. But then I wouldn't be able to setup segues from the cells to other frames, which is really the whole point of using storyboards.
At the end of the day, I want two things: wire up tableview based flows in the storyboard and define cell layouts visually rather than in code. I can't see how to get both of those so far.
As I understand it, you want to:
Unfortunately, there is currently no way to do this. To understand why your previous attempts didn't work, you need to understand more about how storyboards and prototype table view cells work. (If you don't care about why these other attempts didn't work, feel free to leave now. I've got no magical workarounds for you, other than suggesting that you file a bug.)
A storyboard is, in essence, not much more than a collection of .xib files. When you load up a table view controller that has some prototype cells out of a storyboard, here's what happens:
-[UITableView registerNib:forCellReuseIdentifier:]
.-[UITableView dequeueReusableCellWithIdentifier:]
When you request a cell with a given reuse identifier, it checks whether it has a nib registered. If it does, it instantiates an instance of that cell. This is composed of the following steps:
[[CellClass alloc] initWithCoder:]
.-initWithCoder:
method goes through and adds subviews and sets properties that were defined in the nib. (IBOutlet
s probably get hooked up here as well, though I haven't tested that; it may happen in -awakeFromNib
)You configure your cell however you want.
The important thing to note here is there is a distinction between the class of the cell and the visual appearance of the cell. You could create two separate prototype cells of the same class, but with their subviews laid out completely differently. In fact, if you use the default UITableViewCell
styles, this is exactly what's happening. The "Default" style and the "Subtitle" style, for example, are both represented by the same UITableViewCell
class.
This is important: The class of the cell does not have a one-to-one correlation with a particular view hierarchy. The view hierarchy is determined entirely by what's in the prototype cell that was registered with this particular controller.
Note, as well, that the cell's reuse identifier was not registered in some global cell dispensary. The reuse identifier is only used within the context of a single UITableView
instance.
Given this information, let's look at what happened in your above attempts.
In Controller #1, added a prototype cell, set the class to my UITableViewCell subclass, set the reuse id, added the labels and wired them to the class's outlets. In Controller #2, added an empty prototype cell, set it to the same class and reuse id as before. When it runs, the labels never appear when the cells are shown in Controller #2. Works fine in Controller #1.
This is expected. While both cells had the same class, the view hierarchy that was passed to the cell in Controller #2 was entirely devoid of subviews. So you got an empty cell, which is exactly what you put in the prototype.
Designed each cell type in a different NIB and wired up to the appropriate cell class. In storyboard, added an empty prototype cell and set its class and reuse id to refer to my cell class. In controllers' viewDidLoad methods, registered those NIB files for the reuse id. When shown, cells in both controllers were empty like the prototype.
Again, this is expected. The reuse identifier is not shared between storyboard scenes or nibs, so the fact that all of these distinct cells had the same reuse identifier was meaningless. The cell you get back from the tableview will have an appearance that matches the prototype cell in that scene of the storyboard.
This solution was close, though. As you noted, you could just programmatically call -[UITableView registerNib:forCellReuseIdentifier:]
, passing the UINib
containing the cell, and you'd get back that same cell. (This isn't because the prototype was "overriding" the nib; you simply hadn't registered the nib with the tableview, so it was still looking at the nib embedded in the storyboard.) Unfortunately, there's a flaw with this approach — there's no way to hook up storyboard segues to a cell in a standalone nib.
Kept prototypes in both controllers empty and set class and reuse id to my cell class. Constructed the cells' UI entirely in code. Cells work perfectly in all controllers.
Naturally. Hopefully, this is unsurprising.
So, that's why it didn't work. You can design your cells in standalone nibs and use them in multiple storyboard scenes; you just can't currently hook up storyboard segues to those cells. Hopefully, though, you've learned something in the process of reading this.
In spite of the great answer by BJ Homer I feel like I have a solution. As far as my testing goes, it works.
Concept: Create a custom class for the xib cell. There you can wait for a touch event and perform the segue programmatically. Now all we need is a reference to the controller performing the Segue. My solution is to set it in tableView:cellForRowAtIndexPath:
.
I have a DetailedTaskCell.xib
containing a table cell which I'd like to use in multiple table views:
There is a custom class TaskGuessTableCell
for that cell:
This is where the magic happens.
// TaskGuessTableCell.h #import <Foundation/Foundation.h> @interface TaskGuessTableCell : UITableViewCell @property (nonatomic, weak) UIViewController *controller; @end // TashGuessTableCell.m #import "TaskGuessTableCell.h" @implementation TaskGuessTableCell @synthesize controller; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { NSIndexPath *path = [controller.tableView indexPathForCell:self]; [controller.tableView selectRowAtIndexPath:path animated:NO scrollPosition:UITableViewScrollPositionNone]; [controller performSegueWithIdentifier:@"FinishedTask" sender:controller]; [super touchesEnded:touches withEvent:event]; } @end
I have multiple Segues but they all have the same name: "FinishedTask"
. If you need to be flexible here, I suggest to add another property.
The ViewController looks like this:
// LogbookViewController.m #import "LogbookViewController.h" #import "TaskGuessTableCell.h" @implementation LogbookViewController - (void)viewDidLoad { [super viewDidLoad] // register custom nib [self.tableView registerNib:[UINib nibWithNibName:@"DetailedTaskCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"DetailedTaskCell"]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { TaskGuessTableCell *cell; cell = [tableView dequeueReusableCellWithIdentifier:@"DetailedTaskCell"]; cell.controller = self; // <-- the line that matters // if you added the seque property to the cell class, set that one here // cell.segue = @"TheSegueYouNeedToTrigger"; cell.taskTitle.text = [entry title]; // set other outlet values etc. ... return cell; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if([[segue identifier] isEqualToString:@"FinishedTask"]) { // do what you have to do, as usual } } @end
There might be more elegant ways to achieve the same but - it works! :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With