Creating Custom UITableViewCell's with MonoTouch - the correct way

Written on

When I want to create a custom UITableViewCell in a MonoTouch app, I have been using the approach outlined in Simon Guindon's custom UITableViewCell tutorial that he wrote in 2009. I used that method of creating custom cells in my NDC 2010 open-source MonoTouch iPhone app. Craig Dunn's blog is a gold-mine for MonoTouch samples - and he too uses the same approach for creating custom cells.

In fact, almost every MonoTouch sample I have seen on the Internet uses the same approach. It seems to have become the "standard" or "accepted" way of doing custom cells in MonoTouch apps.

For some time now, I have had a feeling that this approach was not optimal and was somehow "wrong". It is essentially a UIViewController wrapped around a cell - the cell's Tag property being used to lookup the cell from a Dictionary. From the samples I have seen (and used myself) the Tag usually comes from Environment.TickCount.

Several things have always struck me as strange with this approach:

  • Sometimes during cell creation, the OS is so fast that the Environment.TickCount is the same for two cells created in quick succession - meaning we get a duplicate key exception for our Dictionary key!
  • I'm not sure we are supposed to be using the cell's Tag property for this purpose.
  • The GetCell method returns a UITableViewCell. Why am I dealing with UIViewController's wrapped around cells?
  • Why do I have to manage my own Dictionary to handle cell reuse - isn't the whole point of the tableView.DequeueReusableCell() method that it handles that for us?

With these questions in the back of my mind, I have always been uneasy about this approach, but never spent much time on improving it.

Miguel de Icaza discusses UITableViewCell's in a blog post, but he doesn't go into detail about how to design the cell in Interface Builder and instantiate it from a XIB file.

One of his comments on that blog post got me thinking, when he said: "I have no idea why anyone would create UIViewControllers for each cell. [...] It seems that those doing it are doing it because that is an easy way of instantiating the cell from a UI created in Interface Builder. They should have instead created a UITableViewCell template in IB, not a UIViewController that contained a cell."

I think this is what he was talking about...

A new approach

Add a new file to your solution for your custom table cell: "iPhone View". Give it the name "MyCustomCell".

NOTE: You are not adding "iPhone View with Controller" here. We aren't going to be seeing any form of UIViewController again :-)

Delete the UIView it gives you by default. Add a UITableViewCell.

With your UITableViewCell selected, go to the Inspector window in Interface Builder, and on the Attributes tab, give your cell an ID in the "Identifier" box. The ID can be anything, but it is important because we will use it again later so that cells can be reused during scrolling.

In the same window, but on the Identity tab, you will see a text box for "class". There, give it the name of your custom cell's class: "MyCustomCell". MonoTouch will generate a partial class with this name when we save and exit Interface Builder, and we will create the other half of that partial class later.

Use Interface Builder to design your cell: you probably want to add some UILabel's and other UI components.

To access those labels programmatically, create outlets for them and connect the outlets to the labels. In the library window, make sure that you are creating the outlets on the class that you chose earlier - and not on anything else (this often catches beginners out - creating the outlets on the wrong object).

You are done with Interface Builder now - so save and quit.

In the MyCustomCell.xib.designer.cs file, MonoTouch will have generated a partial class of type MyCustomCell. Don't touch that code - it will be auto-generated each time you make changes in Interface Builder.

You can and should change the MyCustomCell.xib.cs file, though. Create the other half of that partial class:

public partial class MyCustomCell : UITableViewCell
{
}

Notice that we inherit from UITableViewCell here - not UIViewController. You need to add that inheritance in yourself, because it won't be done automatically for you.

Add two constructors (you will need them both):

public MyCustomCell() : base()
{
}

public MyCustomCell(IntPtr handle) : base(handle)
{
}

I also like to put the logic for binding data to the cell in here rather than exposing each label and other UI components publicly via C# properties. I think this hides the nitty-gritty details from the outside world, and is "better practise" in terms of encapsulation. So I would also add a method like this:

public void BindDataToCell(Product product)
{
    productNameLabel.Text = product.Name;
}

In this example, however, I won't be sending in an object from my model, such as "Product" - I will just send in a string, but you get the idea.

Now jump over to your UITableViewSource class, where you should have a method called GetCell(). It is here where you will usually find the ugliness of Environment.TickCounts, UIViewControllers needlessly wrapped around cells, and Dictionaries hanging on to the cell UIViewControllers. But this time, we will not need any Dictionaries or UIViewControllers. Instead, we will use this code:

public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
    var cell = tableView.DequeueReusableCell("CellID") as MyCustomCell;
    
    if (cell == null)
    {
        cell = new MyCustomCell();
        var views = NSBundle.MainBundle.LoadNib("MyCustomCell", cell, null);
        cell = Runtime.GetNSObject( views.ValueAt(0) ) as MyCustomCell;
    }
    
    cell.BindDataToCell("You are on row " + indexPath.Row);
    
    return cell;
}

You will need this to make it compile:

using MonoTouch.ObjCRuntime;

A simple TableCellFactory

You wouldn't really want to copy and paste that code in every UITableViewSource you have throughout your app - because that would give us lots of code duplication. To avoid that, and add a little bit of elegance the code, I have taken the liberty to create a tiny, light-weight and generically-typed factory class which will take care of this cell-reuse and XIB-loading business for you:

public class TableCellFactory<T> where T : UITableViewCell
{
    private string cellId;
    private string nibName;
    
    public TableCellFactory(string cellId, string nibName)
    {
        this.cellId = cellId;
        this.nibName = nibName;
    }
    
    public T GetCell(UITableView tableView)
    {
        var cell = tableView.DequeueReusableCell(cellId) as T;
            
        if (cell == null)
        {
            cell = Activator.CreateInstance<T>();
            var views = NSBundle.MainBundle.LoadNib(nibName, cell, null);
            cell = Runtime.GetNSObject( views.ValueAt(0) ) as T;
        }
        
        return cell;
    }
}

If you put that somewhere in your iPhone app, you can reduce your GetCell() method to this:

public class MyTableSource : UITableViewSource
{
    private TableCellFactory<MyCustomCell> factory = new TableCellFactory<MyCustomCell>("CellID", "MyCustomCell");
    
    public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
    {
        var cell = factory.GetCell(tableView);
        cell.BindDataToCell("some data");
                
        return cell;
    }
}

I have a fully-working sample over on github, called MonoTouch.CustomTableCells.

These links also helped me out out:

I would appreciate feedback and discussion, and any improvements or tips, so please leave a comment if you have anything to add. Good luck!


Comments

Much better, looks great!

GC

Any clue how to achieve this with Xcode 4? (I haven't figured out how to make outlets without having a corresonding .h file which you can "drag'n'drop labels/etc. to create outlets)

Patrik

@Patrik: Sorry, I've no idea. The whole point of me using MonoTouch is so that I don't have to bother with Xcode and Objective-C :-)

Alex

Hi. I think the last commentor and I am in the same boat. Upgrades to Xcode 4 now there's no separate interfac builder. It's all integrated into Xcode. So when you open a xib file I opens in Xcode 4. Not interfac builder. Any ideas on how to do this in the above situation? Maybe fully through code ?

Peter

@Peter: Okay, I see. I haven't upgraded to Xcode 4 yet, so I can't comment on how to do this with the latest versions. When I do upgrade, I will write a new blog post here, but I can't say when that will be, sorry.

Alex

Hi, I've been using this pattern for a while now, it looks and works great! The TableCellFactory class is part of the shared library I use for every app. Last week I upgraded to MonoTouch 5 and MonoDevelop 2.8 with XCode 4. I've been fiddling with this setup a bit because I couldn't get it working at first. Now, when you create just an iPhone View like in the tutorial, you won't get a C# class, just a .XIB file. So at first there's no way to create outlets in XCode 4/IB. (Check out this tutorial from Xamarin for more info on MT/XCode4 integration http://docs.xamarin.com/ios/getting_started/hello_iphone). Anyway... I figured it out. What you have to do is: 1) Create the new XIB (again, choose New iPhone View, not Controller!) and design it in XCode/IB. 2) Create a new class for your UITableViewCell subclass and [b]make it a partial class[/b] Make sure it has a default constructor and the one receiving IntPtr handle as parameter Now, in order to create and connect the outlets, you have to Ctrl-drag controls to the MonoTouch generated .h file in XCode. However, your manually created class won't get recognized at first, so there's no way to set the Class Identity to that class and there won't be a .h file for it in XCode. Here's the trick: manually put a [Register("classname")] attribute above your class declaration. Double click the XIB again to open it in XCode. Your new UITableViewCell class will be there as a .h file. Now you can create outlets as described in the "Hello iPhone" tutorial. I think the developer experience hasn't improved since Xamarin had to integrate with XCode 4 because IB is now part of XCode. Fiddling around with .h files and Objective-C outlet declarations... Maybe it just takes some time getting used to it. Anyway, hope this helps.

Roy Cornelissen

Thank you very much for this extra info which is very useful.

miliu

Thank you very much! I got as far as the partial class and getting the XIB file created, but was stumped by the lack of the header file.

jseeley

Hello Peter, I followed your instruction to create this TableViewCell with XCode4 and the class gets recognized in the XIB file, but when I run the application I got a null value when I make a reference to an element from the cell (for example a UILabel). Do you have an basic example to share or I need to make additional steps to get the cell working? Thanks.

Jose Gutierrez

I am trying to follow this and have run into one final problem. My view is larger than the default, but when the cells are displayed in the table the table rows are not expanding. I did not change any of the defaults so the properties on the table and cells are all set to Size To Fit. Please help...

Tony

@Tony: If you have dynamic content in your UITableViewCell's, then you need to manually adjust the height of each cell. There is no simple way of doing this and no property you can magically set that stretches everything out. (This is one of the things that sucks about Cocoa. Android and WP7 have better support for this). Firstly, you need to set the Frame of any UILabel's based on the content inside the label. You can call a function called StringSize() and pass in the string content, and the font you are using, and the width of the label, and it will return the correct size for the label (i.e. including the dynamic height, which is what you are interested in). Secondly, if StringSize() tells you that your label was larger than you designed in Interface Builder, in your UITableViewSource you should override GetHeightForRow() and instead of returning 44f (the default) you need to return a new height which takes into account the calculated heights of any inner labels. I am sure if you search for the StringSize() method and GetHeightForRow() you will find an example somewhere. In fact, I think I have done a similar thing on the Twitter tab on my open source NDC 2010 app. Good luck.

Alex

Thanks Peter, but like Jose Gutierrez, I too have null references. Your solution works to create the outlets. But again, any references to like a label are all null. Anyone have any clue here?

Danny

Finally I preferred to create my custom cells according to Miguel de Icaza's post mentioned in this article ... no IB http://tirania.org/monomac/archive/2011/Jan-18.html

Jose Gutierrez

Hello, Firstly thanks for this article. I did everything and it works in simulator. But when i try to run in iPhone device. I m getting error in here : MonoTouch.Foundation.NSBundle.MainBundle.LoadNib ("LanguageCell", this, null); and error is : Objective-C exception thrown. Name: NSUnknownKeyException Reason: [<LanguageCell 0x6e0ccd0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key img_flag. Thanks for help...

Joseph