BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Customizing Tables in MonoTouch

Customizing Tables in MonoTouch

Bookmarks

The source code for the completed example can be found here (for those that want to examine the code without running through the steps, or if you want to compare).

Why Custom Tables?

Tables are a staple in many iPhone applications. Although they’re certainly not a new invention by any means in terms of application development, tables in the iPhone are very specific, given the constraints of the size of the device.

Out of the box, Apple gives you quite a bit of ability to customize your tables through styles. When you first create a table, however, it looks pretty basic. Without any real customizations, there are two basic styles you get for your tables, the default style and the grouped style:

With a few tweaks to the table cells within the table, you can add images and accessories to each table cell as in:

You can even change the font and colors of the table cells, however, sometimes this just isn’t enough. If you truly want to break out of the box and create complex UIs, you have to create your own custom table cells. The following screen shot is an example of just that, an application that uses completely custom table cells to display content:

Fortunately for us, Apple gives us a pretty easy way to do this by allowing us to create our own custom UITableViewCell controls and use them in our UITableView controls. Let’s walk through creating an application that explores working with the UITableView and UITableViewCell controls to create the preceding examples.

Example Application

So let’s create the application. First, open up MonoTouch, it should look something like the following:

Next, create a new iPhone MonoTouch project. To do this, select File : New Solution:

Choose “iPhone Window-based Project” from the C# iPhone templates and let’s name our solution “Example_CustomUITableViewCells”:

Click “OK” on the Project Features dialogue, as they’re not relevant to what we’re doing:

Our project should now look something like this:

Open the MainWindow.xib file in Interface Builder by double-clicking on it in the Solution Explorer window, you should get something that looks like this:

Let’s start by dragging a UITableView control onto our MainWindow, it should now look like:

We need to add an outlet on our AppDelegate class, so that we can access this Table View programmatically.

To do this, in the Library window, select the “Classes” tab at the top, and then “Other Classes” in the drop-down. Then, make sure AppDelegate is selected, and click the “Outlets” tab in the second half of the Library window:

Let’s create an outlet called tblMain, by clicking the “+” button and typing in “tblMain” for the name, your outlets should now look like:

Next, let’s wire up the outlet to the UITableView we added. Select your AppDelegate in the document window, then in the Connections Inspector window, you should see the newly created tblMain outlet:

Wire this outlet up to the our table by dragging from the circle to the right of the outlet in the connections manager to our table in the Document window, or in the Window Designer:

Now that we got our table wired up, let’s switch back over to MonoDevelop and write some code.

Right-click on your project and choose “Add,” and then “New Folder”. Name the folder “Code.”

Next, a class named “BasicTableViewItem.cs.” To do this, right-click on the project again and choose “Add,” and then “New File.” Name the class and click “New:”

In the file, put the following code:

namespace Example_CustomUITableViewCells
{
	//========================================================================
	/// <summary>
	/// Represents our item in the table
	/// </summary>
	public class BasicTableViewItem
	{
		public string Name { get; set; }
		
		public string SubHeading { get; set; }
		
		public string ImageName { get; set; }

		public BasicTableViewItem ()
		{
		}
	}
	//========================================================================
}

This class is fairly simple, it represents an item in a cell and has the following properties:

  • Name – The text that will appear for the item.
  • SubHeading – The text that will appear below the item’s name.
  • ImageName – The name of the image that will appear with the item.

We’ll see how we use this class later.

Next, create another class in the “Code” folder, name it “BasicTableViewItemGroup,” and put the following code in it:

using System;
using System.Collections.Generic;

namespace Example_CustomUITableViewCells
{
	//========================================================================
	/// <summary>
	/// A group that contains table items
	/// </summary>
	public class BasicTableViewItemGroup
	{
		public string Name { get; set; }

		public string Footer { get; set; }
		
		public List<BasicTableViewItem> Items
		{
			get { return this._items; }
			set { this._items = value; }
		}
		protected List<BasicTableViewItem> _items = new List<BasicTableViewItem>();
		
		public BasicTableViewItemGroup ()
		{
		}
	}
	//========================================================================
}

This is also a fairly basic class. It serves as a container for groups of items. It has the following properties:

  • Name – The name of the group. Generally appears at the top of the group.
  • Footer – The text below the group of items.
  • Items – A List<> of the actual BasicTableViewItem objects the group contains. We instantiate this during construction, so that items can be added to it without it having to manually instantiate it.

Next, create another class in the same folder, name it “BasicTableViewSource,” and add the following code to it:

using System;
using System.Collections.Generic;
using MonoTouch.UIKit;

namespace Example_CustomUITableViewCells
{
	//========================================================================
	/// <summary>
	/// Combined DataSource and Delegate for our UITableView
	/// </summary>
	public class BasicTableViewSource : UITableViewSource
	{
		//---- declare vars
		protected List<BasicTableViewItemGroup> _tableItems;
		string _cellIdentifier = "BasicTableViewCell";
		
		public BasicTableViewSource (List<BasicTableViewItemGroup> items)
		{
			this._tableItems = items;
		}
		
		/// <summary>
		/// Called by the TableView to determine how many sections(groups) there are.
		/// </summary>
		public override int NumberOfSections (UITableView tableView)
		{
			return this._tableItems.Count;
		}

		/// <summary>
		/// Called by the TableView to determine how many cells to create for that particular section.
		/// </summary>
		public override int RowsInSection (UITableView tableview, int section)
		{
			return this._tableItems[section].Items.Count;
		}

		/// <summary>
		/// Called by the TableView to retrieve the header text for the particular section(group)
		/// </summary>
		public override string TitleForHeader (UITableView tableView, int section)
		{
			return this._tableItems[section].Name;
		}

		/// <summary>
		/// Called by the TableView to retrieve the footer text for the particular section(group)
		/// </summary>
		public override string TitleForFooter (UITableView tableView, int section)
		{
			return this._tableItems[section].Footer;
		}

		/// <summary>
		/// Called by the TableView to get the actual UITableViewCell to render for the particular section and row
		/// </summary>
		public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
		{
			//---- declare vars
			UITableViewCell cell = tableView.DequeueReusableCell (this._cellIdentifier);
			
			//---- if there are no cells to reuse, create a new one
			if (cell == null)
			{
				cell = new UITableViewCell (UITableViewCellStyle.Default, this._cellIdentifier);
			}
		
			//---- create a shortcut to our item
			BasicTableViewItem item = this._tableItems[indexPath.Section].Items[indexPath.Row];
			
			cell.TextLabel.Text = item.Name;

			return cell;
		}
	}
	//========================================================================
}

The UITableViewSource class is responsible for handling the data binding of our data to our table and also handling the user interaction with the table. In our case, handling user interaction is out of the scope of this article, but we’re still doing our data binding.

This class is a little more complicated than the other two, but let’s break it down:

  • Variable Declarations – We declare two variables, our _tableItems, which will hold the actual table items that populate the table, and the _cellIdentifier, which we will use later to provide a key in which to reuse our table cells.
  • Constructor – Our constructor takes a parameter called items of type List<BasicTableViewItemGroup>. In our example, we’re forcing our class to be instantiated with our groups of items, so that we know they’re valid.
  • NumberOfSections – This method is called by the table to retrieve the number of sections/groups to create in the table. In this case, we simply return the count of groups we have in our _tableItems.
  • RowsInSection – This method is called by the table to retrieve the number of items in a particular section/group to create in the table. We return the number of items in the particular group index.
  • TitleForHeader – This method is called by the table to retrieve the text that is displayed for the section/group header. We return the Name property for the BasicTableViewItemGroup in that section.
  • TitleForFooter – This method is called by the table to retrieve the text that is displayed below the section/group of items. We do the same thing here as TitleForHeader, except that this time, we return the Footer property.
  • GetCell – This is method is called by the table to retrieve the actual UITableCell control for a particular section/group and row, and is where the bulk of the work is done for data binding. The first thing we do here is to call DequeueReusableCell on the on the associated UITableView. This is done for performance reasons. Because the iPhone has limited processing power, if it had to create new UITableViewCell controls each time it had to display one, the overhead would cause a significant slow down on long lists. To counteract this, under the hood, the UITableView control keeps a pool of UITableViewCell controls, and when a cell scrolls out of view, it goes into the pool for reuse. This is where our _cellIdentifier comes into play. By giving our cell an identifier, they essentially become templates that we can reuse by pulling them out of the cell pool. At first, there won’t be any cells to reuse however, so in the next line, we check to see if the cell is null, and if it is, we create a new UITableViewCell control, specifying it’s style, and the CellIdentifier.

Ok, now that we’ve got those classes out of the way, we’re going to add some images so we’ve got something to display with the items.

First, let’s create a folder in the project called “Images.” Do this the same way you created the “Code” folder from before. Save the following images to your hard drive with the following names, respectively:

Image

Name

PawIcon.png

LightBulbIcon.png

PushPinIcon.png

TargetIcon.png

Once you have those saved, let’s add them to your project. Right-click on your “Images” folder and choose “Add Files.” Select the images that you saved, and check “Override default build action,” and select “Content” in the drop-down. This is very important. Content is the only build action that will allow you to load them and use them at run time.

Your solution should now look like this:

Make sure and build your project to make sure you’ve done everything correctly so far. To build, hold down the Apple Key and press the “B” button, or choose “Build All” from the Build menu.

All right, now that we have our classes and our images, let’s go edit our application code to use them!

Double click on Main.cs to bring the code editor up. In your AppDelegate class, add the following line:

BasicTableViewSource _tableViewSource; 

This is a class variable to hold our BasicTableViewSource that we created earlier.

Next, put the following method in your AppDelegate class:

/// <summary>
/// Creates a set of table items.
/// </summary>
protected void CreateTableItems ()
{
	List<BasicTableViewItemGroup> tableItems = new List<BasicTableViewItemGroup> ();
	
	//---- declare vars
	BasicTableViewItemGroup tGroup;
	
	//---- birds
	tGroup = new BasicTableViewItemGroup() { Name = "Birds", Footer = "Birds have wings, and sometimes use them." };
	tGroup.Items.Add (new BasicTableViewItem() { Name = "Crow", SubHeading = "AKA, Raven.", ImageName = "PawIcon.png" });
	tGroup.Items.Add (new BasicTableViewItem() { Name = "Chicken", SubHeading = "Males are called roosters.", ImageName = "PushPinIcon.png" });
	tGroup.Items.Add (new BasicTableViewItem() { Name = "Turkey", SubHeading = "Eaten at thanksgiving.", ImageName = "LightBulbIcon.png" });
	tableItems.Add (tGroup);

	//---- fish
	tGroup = new BasicTableViewItemGroup() { Name = "Fish", Footer = "Fish live in water. Mostly." };
	tGroup.Items.Add (new BasicTableViewItem() { Name = "Trout", SubHeading = "Rainbow is a popular kind.", ImageName = "TargetIcon.png" });
	tGroup.Items.Add (new BasicTableViewItem() { Name = "Salmon", SubHeading = "Good sushi.", ImageName = "PawIcon.png" });
	tGroup.Items.Add (new BasicTableViewItem() { Name = "Cod", SubHeading = "Flat fish.", ImageName = "LightBulbIcon.png" });
	tableItems.Add (tGroup);

	//---- mammals
	tGroup = new BasicTableViewItemGroup() { Name = "Mammals", Footer = "Mammals nurse their young." };
	tGroup.Items.Add (new BasicTableViewItem() { Name = "Deer", SubHeading = "Bambi.", ImageName = "PushPinIcon.png" });
	tGroup.Items.Add (new BasicTableViewItem() { Name = "Bats", SubHeading = "Fly at night.", ImageName = "TargetIcon.png" });
	tableItems.Add (tGroup);
	
	this._tableViewSource = new BasicTableViewSource(tableItems);
}

We’ll call this method to create our data source complete with some data to display in our table.

Finally, in your FinishedLaunching method of your AppDelegate class, add the following code before window.MakeKeyAndVisible is called.

this.CreateTableItems (); 
this.tblMain.Source = this._tableViewSource; 

This calls the method that we just added to create our table data source, and then assign that data source to our table.

Your finished Main.cs should now look like:

using System;
using System.Collections.Generic;
using System.Linq;
using MonoTouch.Foundation;
using MonoTouch.UIKit;

namespace Example_CustomUITableViewCells
{
	public class Application
	{
		static void Main (string[] args)
		{
			UIApplication.Main (args);
		}
	}

	// The name AppDelegate is referenced in the MainWindow.xib file.
	public partial class AppDelegate : UIApplicationDelegate
	{
		BasicTableViewSource _tableViewSource;
		
		// This method is invoked when the application has loaded its UI and its ready to run
		public override bool FinishedLaunching (UIApplication app, NSDictionary options)
		{
			this.CreateTableItems ();
			this.tblMain.Source = this._tableViewSource;
	
			// If you have defined a view, add it here:
			// window.AddSubview (navigationController.View);
			window.MakeKeyAndVisible ();
			
			return true;
		}

		// This method is required in iPhoneOS 3.0
		public override void OnActivated (UIApplication application)
		{
		}
		
		/// <summary>
		/// Creates a set of table items.
		/// </summary>
		protected void CreateTableItems ()
		{
			List<BasicTableViewItemGroup> tableItems = new List<BasicTableViewItemGroup> ();
			
			//---- declare vars
			BasicTableViewItemGroup tGroup;
			
			//---- birds
			tGroup = new BasicTableViewItemGroup() { Name = "Birds", Footer = "Birds have wings, and sometimes use them." };
			tGroup.Items.Add (new BasicTableViewItem() { Name = "Crow", SubHeading = "AKA, Raven.", ImageName = "PawIcon.png" });
			tGroup.Items.Add (new BasicTableViewItem() { Name = "Chicken", SubHeading = "Males are called roosters.", ImageName = "PushPinIcon.png" });
			tGroup.Items.Add (new BasicTableViewItem() { Name = "Turkey", SubHeading = "Eaten at thanksgiving.", ImageName = "LightBulbIcon.png" });
			tableItems.Add (tGroup);
		
			//---- fish
			tGroup = new BasicTableViewItemGroup() { Name = "Fish", Footer = "Fish live in water. Mostly." };
			tGroup.Items.Add (new BasicTableViewItem() { Name = "Trout", SubHeading = "Rainbow is a popular kind.", ImageName = "TargetIcon.png" });
			tGroup.Items.Add (new BasicTableViewItem() { Name = "Salmon", SubHeading = "Good sushi.", ImageName = "PawIcon.png" });
			tGroup.Items.Add (new BasicTableViewItem() { Name = "Cod", SubHeading = "Flat fish.", ImageName = "LightBulbIcon.png" });
			tableItems.Add (tGroup);
		
			//---- mammals
			tGroup = new BasicTableViewItemGroup() { Name = "Mammals", Footer = "Mammals nurse their young." };
			tGroup.Items.Add (new BasicTableViewItem() { Name = "Deer", SubHeading = "Bambi.", ImageName = "PushPinIcon.png" });
			tGroup.Items.Add (new BasicTableViewItem() { Name = "Bats", SubHeading = "Fly at night.", ImageName = "TargetIcon.png" });
			tableItems.Add (tGroup);
			
			this._tableViewSource = new BasicTableViewSource(tableItems);
		}
	
	}
}

Let’s run the app and see what it looks like. To run the app, hold down the Apple key and press the “Enter” button, or “Debug” from the Run menu.

When we run it, we should see:

The footer looks a little weird in this mode. Let’s change the table type to Grouped and re-run it. To do this, let’s open the MainWindow.xib back up in Interface Builder (by double-clicking it), and edit the table’s style. Click on the Table View in the Document window, and then in the Inspector window, change the style drop-down to “Grouped:”

Save the file, switch back to MonoDevelop and run the application again, you should now see:

The footers now make a little more sense.

Let’s play with some other stuff. We, have an image and a sub-heading on our item, so let’s utilize them.

Stop the application and then double-click our BasicTableViewSource.cs class to edit it. Change this line, in our GetCell method:

cell = new UITableViewCell (UITableViewCellStyle.Default, this._cellIdentifier); 

to this:

cell = new UITableViewCell (UITableViewCellStyle.Subtitle, this._cellIdentifier); 

This changes the style of the UITableViewCell to the Subtitle style, which displays a second line of text within the cell.

There are a few styles that you can play with. Note, however, using images is only supported in the Default and Subtitle styles.

Now, just after this line:

cell.TextLabel.Text = item.Name; 

add:

cell.Accessory = UITableViewCellAccessory.Checkmark;
cell.DetailTextLabel.Text = item.SubHeading;
if(!string.IsNullOrEmpty(item.ImageName))
{
	cell.ImageView.Image = UIImage.FromFile("Images/" + item.ImageName );
}

The out of the box UITableViewCells are laid out as follows:

There are two main parts to a UITableViewCell control, out of the box, the cell body, which is the entire area that the cell takes up, and the cell content, which is what we generally are allowed to put content in. If an accessory is not used, the cell content expands to take up the entire cell body. Similarily, if an image is not used, the label area overtakes the image are.

Let’s look at how we use this in our code. The first line tells the cell to have a Checkmark accessory in the right portion of the cell. Then we add our SubHeading text to the DetailTextLabel. Finally, we set an image on the cell.

It’s easy to create an image from our files. We simply use the UIImage.FromFile constructor, and pass it the path to our image. By setting our build type to Content on the images earlier, we can access them as if our folder structure in the project was our file system.

Running the application now, should get us the following:

Pretty cool. If we switch the table style in Interface Builder back to Plain from Grouped (open MainWindow.xib up by double-clicking, select our table, and change the style to “Plain” in the drop-down, and save), and run it again, we get:

Again pretty cool.

Since the 3.0 release of the iPhone OS, we’ve also been able to customize a number of other things including TextColor, SelectedTextColor, SelectedImage, BackgroundView, SelectedBackgroundView, and others. These give you a number of possibilities, but sometimes we want something radically different.

In order to accomplish this, we have to create our own table cell altogether. Apple allows us do this by inheriting from UITableViewCell and creating our own content, any way we want.

So let’s do just that. First, create a new folder in your project called “Controls.” In it, we’re going to create a new View with View Controller. Right-click on the Controls folder and select “Add,” and then “New File.” Choose “iPhone” from the left-pane and “View Interface Definition with Controller” on the right and name it “CustomTableViewCell:”

Open your CustomTableViewCell.xib file in Interface Builder by double-clicking it. It should be a blank view:

We don’t want a standard view, we want to create a UITableViewCell, so we’re going to delete the view and replace it with a cell.

Delete the View from your Document window by holding down the Apple key and press the Backspace key, or choose “Delete” from the edit window.

Next, drag a Table View Cell from the Library window onto your Document:

If you double-click on your newly added table cell, it’ll open up in the designer, and you can see it gives us a surface on which to add controls:

Let’s use Interface Builder to design our custom cell now. Make it a little taller by dragging the resize handle in the bottom right. Then, from the Library window, drag on a UIImageView and a couple of UILabels:

You can use the Attributes Inspector window to change the size of the UILabel controls, as well as the color, etc. It doesn’t have to look exactly like this, just as long as the elements are there.

Now that we have our custom design, let’s add outlets for our custom cell and it’s contents on the File’s Owner so that we can access them. Before, we added outlets directly to the App Delegate, but typically, we add them to File’s Owner, so that they get created on the class that owns the view.

To do this, select “File’s Owner” in the document window, then in the Library window, select the “Classes” tab at the top, and the “Other Classes” item in the drop-down. Select our CustomTableViewCell class, and then select the “Outlets” tab in the second half of the window. Add Outlets called “cellMain,” “lblHeading,” “lblSubHeading,” and “imgMain:”

Wire these outlets up to the controls that you previously created, just like you did before with our table.

Make sure “File’s Owner” is selected in the Document window and drag your control from the Connection Inspector to the control in the Document window:

The last thing we have to do in Interface Builder is to provide a cell identifier. Click on “Table View Cell” in the Document window, then in the Attributes Inspector, set the Identifier property to “MyCustomTableCellView:”

This allows our custom cell template to be reused, as we did before in code.

We’re done in Interface Builder, so save your file and then go back to MonoDevelop. Double-click on CustomTableCellView.xib.cs to open the class file.

We’re going to modify this class a little bit. First, let’s add the following properties to the class:

public UITableViewCell Cell
{
	get { return this.cellMain; }
}

public string Heading
{
	get { return this.lblHeading.Text; }
	set { this.lblHeading.Text = value; }
}

public string SubHeading
{
	get { return this.lblSubHeading.Text; }
	set { this.lblSubHeading.Text = value; }
}

public UIImage Image
{
	get { return this.imgMain.Image; }
	set { this.imgMain.Image = value; }
}

Let’s take a look at these properties:

  • Cell – This property gives the consumer of our custom cell direct access to the cell itself, so that in the GetCell call in our UITableViewSource, we simple pull the cell directly from this property, as we’ll see in a bit.
  • Heading and SubHeading – These properties expose direct access to the text of our UILabel controls in our table cell.
  •  Image – This property directly exposes the UIImage control of our table cell.

Next, we need to modify one of the constructors. The last constructor in file looks like this, when the file is created by MonoDevelop:

public CustomTableViewCell () : base("CustomTableViewCell", null)
{
	Initialize ();
}

This seems a fairly straightforward constructor, if you read the Mono documentation on the base constructor that it’s calling (found here) it says that it initializes the class and loads the Nib (compiled .xib file) with the the name that we passed in. What the documentation fails to mention however, is that the base class constructor is asynchronous. If we use this call as is, the Nib will likely not actually get loaded immediately, and when we go to set our properties such as Heading, on our custom cell, we’ll get the dreaded null-reference error.

In order to fix that, we have to make sure the Nib file gets loaded synchronously, and the method doesn’t return until it has been loaded.

To do this, modify your constructor so that it looks like this.

public CustomTableViewCell ()// : base("CustomTableViewCell", null)
{
	//---- this next line forces the loading of the xib file to be synchronous
	MonoTouch.Foundation.NSBundle.MainBundle.LoadNib ("CustomTableViewCell", this, null);
	Initialize ();
}

The first thing that we’re doing here is to comment out the call to the base-class constructor. We’re going to manually make this call, so we don’t need an extra one here. Next, we added a call to the LoadNib method. This does the exact same thing that our base-class constructor did, except that it forces it to be synchronous. Now, we’re assured that the Nib, and everything in it, will be initialized so that we can access it.

Our completed class should now look something like the following:

using System;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using System;

namespace Example_CustomUITableViewCells
{
	//========================================================================
	public partial class CustomTableViewCell : UIViewController
	{
		#region Constructors

		// The IntPtr and initWithCoder constructors are required for controllers that need 
		// to be able to be created from a xib rather than from managed code

		public CustomTableViewCell (IntPtr handle) : base(handle)
		{
			Initialize ();
		}

		[Export("initWithCoder:")]
		public CustomTableViewCell (NSCoder coder) : base(coder)
		{
			Initialize ();
		}

		public CustomTableViewCell ()// : base("CustomTableViewCell", null)
		{
			//---- this next line forces the loading of the xib file to be synchronous
			MonoTouch.Foundation.NSBundle.MainBundle.LoadNib ("CustomTableViewCell", this, null);
			Initialize ();
		}

		void Initialize ()
		{
		}
		
		#endregion
		
		
		public UITableViewCell Cell
		{
			get { return this.cellMain; }
		}
		
		public string Heading
		{
			get { return this.lblHeading.Text; }
			set { this.lblHeading.Text = value; }
		}
		
		public string SubHeading
		{
			get { return this.lblSubHeading.Text; }
			set { this.lblSubHeading.Text = value; }
		}
		
		public UIImage Image
		{
			get { return this.imgMain.Image; }
			set { this.imgMain.Image = value; }
		}
		
	}
	//========================================================================
}

Ok, now that we have our custom cell class all figured out, let’s go and build a custom UITableViewSource class for it.

Create a new class in the “Code” folder, and copy the following into it:

using System;
using System.Collections.Generic;
using MonoTouch.UIKit;

namespace Example_CustomUITableViewCells
{
	//========================================================================
	/// <summary>
	/// Combined DataSource and Delegate for our UITableView with custom cells
	/// </summary>
	public class CustomTableViewSource : UITableViewSource
	{
		//---- declare vars
		protected List<BasicTableViewItemGroup> _tableItems;
		protected string _customCellIdentifier = "MyCustomTableCellView";
		protected Dictionary<int, CustomTableViewCell> _cellControllers = 
			new Dictionary<int, CustomTableViewCell>();
		
		public CustomTableViewSource (List<BasicTableViewItemGroup> items)
		{
			this._tableItems = items;
		}

		/// <summary>
		/// Called by the TableView to determine how many sections(groups) there are.
		/// </summary>
		public override int NumberOfSections (UITableView tableView)
		{
			return this._tableItems.Count;
		}

		/// <summary>
		/// Called by the TableView to determine how many cells to create for that particular section.
		/// </summary>
		public override int RowsInSection (UITableView tableview, int section)
		{
			return this._tableItems[section].Items.Count;
		}

		/// <summary>
		/// Called by the TableView to retrieve the header text for the particular section(group)
		/// </summary>
		public override string TitleForHeader (UITableView tableView, int section)
		{
			return this._tableItems[section].Name;
		}

		/// <summary>
		/// Called by the TableView to retrieve the footer text for the particular section(group)
		/// </summary>
		public override string TitleForFooter (UITableView tableView, int section)
		{
			return this._tableItems[section].Footer;
		}

		/// <summary>
		/// Called by the TableView to retreive the height of the row for the particular section and row
		/// </summary>
		public override float GetHeightForRow (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
		{
			return 109f;
		}

		/// <summary>
		/// Called by the TableView to get the actual UITableViewCell to render for the particular section and row
		/// </summary>
		public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
		{
			//---- declare vars
			UITableViewCell cell = tableView.DequeueReusableCell (this._customCellIdentifier);
			CustomTableViewCell customCellController = null;
			
			//---- if there are no cells to reuse, create a new one
			if (cell == null)
			{
				customCellController = new CustomTableViewCell ();
				// retreive the cell from our custom cell controller
				cell = customCellController.Cell;
				// give the cell a unique ID, so we can match it up to the controller
				cell.Tag = Environment.TickCount;
				// store our controller with the unique ID we gave our cell
				this._cellControllers.Add (cell.Tag, customCellController);
			}
			else
			{
				// retreive our controller via it's unique ID
				customCellController = this._cellControllers[cell.Tag];
			}
			
			//---- create a shortcut to our item
			BasicTableViewItem item = this._tableItems[indexPath.Section].Items[indexPath.Row];
			
			//---- set our cell properties
			customCellController.Heading = item.Name;
			customCellController.SubHeading = item.SubHeading;
			if (!string.IsNullOrEmpty (item.ImageName))
			{
				customCellController.Image = UIImage.FromFile ("Images/" + item.ImageName);
			}
			
			//---- return the custom cell
			return cell;
		}

	}
	//========================================================================}
}

If you examine this, it looks almost identical to our BasicTableViewSource class but with a few changes.

The first thing to note is our declarations:

//---- declare vars
protected List<BasicTableViewItemGroup> _tableItems;
protected string _customCellIdentifier = "MyCustomTableCellView";
protected Dictionary<int, CustomTableViewCell> _cellControllers = 
	new Dictionary<int, CustomTableViewCell>();

We’ve added a new item called _cellControllers. We’ll see how we use this later, but it will hold a collection of our customized cell controllers.

The next addition is the GetHeightForRow method:

/// <summary>
/// Called by the TableView to retreive the height of the row for the particular section and row
/// </summary>
public override float GetHeightForRow (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
	return 109f;
}

When we created our custom cell controller, we made it taller than the usual cell. During databinding, CocoaTouch needs to know how much room to allocate for our cell. It does this by calling the GetHeightForRow method. If we didn’t tell it our new size, it would try to fit it in the standard size cell area, and we’d see weird results.

If we open up our cell in Interface Builder and look at the Size Inspector, it tells us the height of our cell. In my case, it’s 109 pixels, but yours may be slightly different.

The final piece that has changed is our GetCell method:

/// <summary>
/// Called by the TableView to get the actual UITableViewCell to render for the particular section and row
/// </summary>
public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
	//---- declare vars
	UITableViewCell cell = tableView.DequeueReusableCell (this._customCellIdentifier);
	CustomTableViewCell customCellController = null;
	
	//---- if there are no cells to reuse, create a new one
	if (cell == null)
	{
		customCellController = new CustomTableViewCell ();
		// retreive the cell from our custom cell controller
		cell = customCellController.Cell;
		// give the cell a unique ID, so we can match it up to the controller
		cell.Tag = Environment.TickCount;
		// store our controller with the unique ID we gave our cell
		this._cellControllers.Add (cell.Tag, customCellController);
	}
	else
	{
		// retreive our controller via it's unique ID
		customCellController = this._cellControllers[cell.Tag];
	}
	
	//---- create a shortcut to our item
	BasicTableViewItem item = this._tableItems[indexPath.Section].Items[indexPath.Row];
	
	//---- set our cell properties
	customCellController.Heading = item.Name;
	customCellController.SubHeading = item.SubHeading;
	if (!string.IsNullOrEmpty (item.ImageName))
	{
		customCellController.Image = UIImage.FromFile ("Images/" + item.ImageName);
	}
	
	//---- return the custom cell
	return cell;
}

This method starts out very similar to our BasicTableViewSource class, we try and pull a cell to reuse from the pool via the DequeueReusableCell call. After this, things get a little more complicated. If we can’t get a cell to reuse, we have to create a new one. This starts by creating the our CustomTableViewCell class which is the controller that actually contains our custom cell.

Once we’ve created our controller, we assign the cell variable to be the Cell property of our controller.

We then add a unique identifier to our cell, so that we can associate it with the correct CustomTableViewCell controller later. We’re using Environment.TickCount because it provides a reasonable level of uniqueness. If we wanted to, we could use the Guid.NewGuid, but that’s a fairly expensive call.

Next, we use that same tag identifier that we created to store the CustomTableViewCell controller in a Dictionary object.

The reason why we do this become fairly obvious in the next lines:

else
{
	// retreive our controller via it's unique ID
	customCellController = this._cellControllers[cell.Tag];
}

If we did find a cell to reuse, we need to set its properties via the controller. In order to do this, we have to pull it out of our Dictionary using the identifier that we used before.

That covers the bulk of changes to this class. The next few lines underwent minor changes in order to change the properties on the controller class, rather than the cell itself.

Ok! We’re almost done. Open up Main.cs, and we’re going to make two changes in our AppDelegate class to use our CustomTableViewSource instead of the BasicTableViewSource.

First, comment out this line:

BasicTableViewSource _tableViewSource; 

And add this line, right after it:

CustomTableViewSource _tableViewSource; 

Next, in our CreateTableItems method, comment out this line:

this._tableViewSource = new BasicTableViewSource(tableItems); 

And add this line, right after it:

this._tableViewSource = new CustomTableViewSource(tableItems); 

Our updated AppDelegate class should now look like:

// The name AppDelegate is referenced in the MainWindow.xib file.
public partial class AppDelegate : UIApplicationDelegate
{
	//BasicTableViewSource _tableViewSource;
	CustomTableViewSource _tableViewSource;
	
	// This method is invoked when the application has loaded its UI and its ready to run
	public override bool FinishedLaunching (UIApplication app, NSDictionary options)
	{
		this.CreateTableItems ();
		this.tblMain.Source = this._tableViewSource;

		// If you have defined a view, add it here:
		// window.AddSubview (navigationController.View);
		window.MakeKeyAndVisible ();
		
		return true;
	}

	// This method is required in iPhoneOS 3.0
	public override void OnActivated (UIApplication application)
	{
	}
	
	/// <summary>
	/// Creates a set of table items.
	/// </summary>
	protected void CreateTableItems ()
	{
		List<BasicTableViewItemGroup> tableItems = new List<BasicTableViewItemGroup> ();
		
		//---- declare vars
		BasicTableViewItemGroup tGroup;
		
		//---- birds
		tGroup = new BasicTableViewItemGroup() { Name = "Birds", Footer = "Birds have wings, and sometimes use them." };
		tGroup.Items.Add (new BasicTableViewItem() { Name = "Crow", SubHeading = "AKA, Raven.", ImageName = "PawIcon.png" });
		tGroup.Items.Add (new BasicTableViewItem() { Name = "Chicken", SubHeading = "Males are called roosters.", ImageName = "PushPinIcon.png" });
		tGroup.Items.Add (new BasicTableViewItem() { Name = "Turkey", SubHeading = "Eaten at thanksgiving.", ImageName = "LightBulbIcon.png" });
		tableItems.Add (tGroup);
	
		//---- fish
		tGroup = new BasicTableViewItemGroup() { Name = "Fish", Footer = "Fish live in water. Mostly." };
		tGroup.Items.Add (new BasicTableViewItem() { Name = "Trout", SubHeading = "Rainbow is a popular kind.", ImageName = "TargetIcon.png" });
		tGroup.Items.Add (new BasicTableViewItem() { Name = "Salmon", SubHeading = "Good sushi.", ImageName = "PawIcon.png" });
		tGroup.Items.Add (new BasicTableViewItem() { Name = "Cod", SubHeading = "Flat fish.", ImageName = "LightBulbIcon.png" });
		tableItems.Add (tGroup);
	
		//---- mammals
		tGroup = new BasicTableViewItemGroup() { Name = "Mammals", Footer = "Mammals nurse their young." };
		tGroup.Items.Add (new BasicTableViewItem() { Name = "Deer", SubHeading = "Bambi.", ImageName = "PushPinIcon.png" });
		tGroup.Items.Add (new BasicTableViewItem() { Name = "Bats", SubHeading = "Fly at night.", ImageName = "TargetIcon.png" });
		tableItems.Add (tGroup);
		
		//this._tableViewSource = new BasicTableViewSource(tableItems);
		this._tableViewSource = new CustomTableViewSource(tableItems);
	}
}		

Run the application, and you should now get this:

If you change your table style to Grouped in Interface Builder, save your changes, and then run it again, you should get something like:

Congratulations! That was a lot of work, but you know how to customize tables with MonoTouch. Now that we’ve got this all working, let’s look at a few more considerations when working with custom cells.

Performance Considerations

If not done properly, using the UITableView can have tremendous performance issues. This is especially true if you have many rows. The iPhone, while impressive, has a limited processor, and in order to get the responsiveness that users expect with iPhone applications, there are several performance optimizations that you should consider.

Additionally, it is advisable to deploy to the device early and often and evaluate the performance of your table. The simulator is just that, a simulator, and not the actual device. It operates much faster than the device does, and if you rely on it to gauge the performance of your application, you will almost certainly be disappointed when you deploy to the device.

Furthermore, when you do test your tables, if the users can add their own rows, you should test your application with many more rows than you expect a user to add. If it is still snappy and responsive, you know that you’ve built it correctly, and your users won’t be disappointed with its performance.

  • Cell Reuse – Cell reuse is the first technique to employ to have a responsive table. This is especially true if you have lots of rows. If you do not reuse your cells, the iPhone OS has to create new ones each time one is displayed. This can become a huge problem very quickly. In order to determine if your cells are being reused, it is advisable to use a Console.WriteLine call in your cell reuse code to write out to the application console when a cell is being reused when it scrolls onto the page. Make sure you have enough rows that some first appear off the screen, so that when they scroll onto he screen, the OS has an opportunity to reuse old ones that have already scrolled off. The examples in this article show how to properly reuse cells, if your cells aren’t being reused, study the code samples and make sure your implementation is correct.
  • Cache the Row Height – The table can request the height of your rows quite often. In fact, it will likely request it, at least every time the cell is created, and in some cases can request it more often than that. If you are calculating the row height based on cell contents, be sure and cache that value, so you don’t have to recalculate it. You may want to create a property on your custom cell controller, in fact, that calculates, and then caches it.
  • Cache Images – If you’re using images, consider caching them so that they don't have to be loaded from a file each time they’re displayed. If you’re cells share images, consider putting them into a Dictionary object and pull them from there. Be wary of loading too many though. It’s not unusual to only have half of the iPhone’s useable memory at your disposal, so if you have a lot of images, you may have no choice to load them from file, or at least have a collection where you only store a certain number, and load the least used from files.
  • Stay Away from Transparency – One of the most expensive operations to perform on the iPhone is the rendering of transparency. There are two drawing systems on the iPhone, CoreGraphics, and CoreAnimation. CoreGraphics utilizes the GPU and CoreAnimation utilizes either the main processor, or the GPU, depending what it thinks will be fastest, and usually this is the GPU. The problem with this is that the iPhone’s GPU is not optimized for blending. Because of this, you should try and avoid transparency where possible. If you absolutely cannot avoid transparency, you should do the blending yourself by overriding the DrawRect function, and handling the drawing yourself, as we see in the next bullet point.
  • Manually Draw the Cell in DrawRect – As a last ditch effort for performance, you can override the DrawRect method and perform the drawing yourself. This has it’s drawbacks however. First, it can be technically complex, and second, it can utilize a LOT of memory. As such, it’s much more suited to tables that don’t have a lot of rows, but require lots of processing power to create. For more information on this technique, check out the fifth example in Apple’s sample app TableViewSuite, noted below.
  • Avoid Complex Graphical Computations – As with the transparency issue, stay away from graphical elements that require computation, such as a gradient that isn’t already baked into the an image your displaying.
  • Create your Cell Programmatically – If you’re running out of optimizations, and you’re still needing a performance boost, you can scrap the custom UITableViewCell that you created in Interface Builder altogether and hand-create the controls programmatically. For more information about building views programmatically, check out Craig Dunn’s blog post. For further reading on creating MonoTouch apps without Interface Builder, checkout the articles listed on MonoTouch.info about it, here: http://monotouch.info/Tags/NoIB

For more examples showing performance optimizations, check out Apple’s TableViewSuite example application.

It’s written in objective-c, but the concepts are the same.

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

  • Object reference not set to an intense of an object

    by Ron Herhuth,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    I got the first part of the tutorial working fine...but when I tried to create the custom cell (5 times) I can't seem to get it working. I get an object reference not set to an instance of an object exception whenever it hits this code block:

    public UITableViewCell Cell
    {
    get { return this.cellMain; }
    }

    public string Heading
    {
    get { return this.lblHeading.Text; }
    set { this.lblHeading.Text = value; }
    }

    public string SubHeading
    {
    get { return this.lblSubHeading.Text; }
    set { this.lblSubHeading.Text = value; }
    }

    public UIImage Image
    {
    get { return this.imgMain.Image; }
    set { this.imgMain.Image = value; }
    }

    It is like the class is not connecting up with the designer code, but I followed the instructions several times all with the same results. Any assistance would be greatly appreciated.

    Thanks,
    Ron

  • Re: Object reference not set to an intense of an object

    by bryan costanich,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Hey ron, quick question for you 1) did you get the constructor change in? and 2) does the sample code work for you?

  • Re: Object reference not set to an intense of an object

    by Ron Herhuth,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    I got it working...I forgot the constructor change.

    Question I'm trying to no avail to build a custom menu in ib and then try to show it over the current view...it should just occupy a fraction of the parent view. I can't seem to make this work...it shows in the center but the views controls won't show up. I need to figure out how to display the subview at a specified size and location...and display it's controls...do you know how I can achieve this? Do you know of any tutorials that illustrate this? Please note that I don't want to use the navigation controller and swap views...I want the view to be a floating menu over the current view.

    Thank you!
    Ron

  • Use within a sub view

    by Stuart McVicar,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Hi,
    I found this a very useful article, thanks for posting it. I tried to implement this as a subview of a view, so I had 2 subviews within one view, the view with the table appeared on the bottom view on a main view within the app (this is an iPad app). The table is constructed ok, but when I try to scroll down the table I get a:

    CustomTableViewSource respondsToSelector:]: message sent to deallocated instance 0xcd06560

    When I implement the table in a separate view the table works perfectly..

    Do you know if it is possible to implement the table in this way, or even better do you know why it's trying to send a message to a deallocated instance?

    Many Thanks,
    Stuart

  • Regarding LoadNib

    by Itay Adler,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    First of all great article, really helped me for what I needed.
    I had a little issue, where it seems that when the field name of the cell was 'view', then
    the property would yield null, as if it wasn't loaded yet, but all other fields were loaded.
    So when I renamed the field name from 'view' to something else(All in Interface Builder of course) then it worked
    like a charm.

    Just thought it was worth mentioning if anyone else encountered it.

    Thanks!

  • Custom View Doesn't run

    by Ajay Kr,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    When I added the Custom View it is not doing anything after doing a successfull build!! I have checked the code thoroughly against supplied code and debugged it and can't understand what is happening. Using Monodevelop 2.4.2 version. Would appreciate any input.

    thanks

  • Problem in device

    by Joseph Hunter,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    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...</languagecell>

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT