Storyboarding is an exciting new feature in iOS 5 that will save you a lot of time building user interfaces for your apps.
This article was adapted from Beginning Storyboards in iOS 5 Part 1, Matthijs Hollemans, by permission of the publisher, Ray Wenderlich. Please visit the Ray Wenderlich iOS Tutorials site for this and many other fine iOS tutorials.
The vast majority of the text of this article is from the original article. Rather than setting the original material in block quotes or double quotes, we’ve chosen instead to highlight the MonoTouch-specific material by setting it in bold.
Until now, my iOS apps have been NIB-free. I wasn’t convinced of the value of using IB until reading the original article on the Ray Wenderlich site. One of my development goals is to increase my productivity. I hope that MonoTouch and storyboards will help achieve that goal.
To show you what a storyboard is, I’ll (Note: being in non-bold, that’s Matthijs Hollemans, the author of the original article, writing in the first person) let a picture do the talking. This is the storyboard that we will be building in this tutorial:
You may not know exactly yet what the app does but you can clearly see which screens it has and how they are related. That is the power of using storyboards.
If you have an app with many different screens then storyboards can help reduce the amount of glue code you have to write to go from one screen to the next. Instead of using a separate nib file for each view controller, your app uses a single storyboard that contains the designs of all of these view controllers and the relationships between them.
Storyboards have a number of advantages over regular nibs:
- With a storyboard you have a better conceptual overview of all the screens in your app and the connections between them. It’s easier to keep track of everything because the entire design is in a single file, rather than spread out over many separate nibs.
- The storyboard describes the transitions between the various screens. These transitions are called “segues” and you create them by simply ctrl-dragging from one view controller to the next. Thanks to segues you need less code to take care of your UI.
- Storyboards make working with table views a lot easier with the new prototype cells and static cells features. You can design your table views almost completely in the storyboard editor, something else that cuts down on the amount of code you have to write.
Not everything is perfect, of course, and storyboards do have some limitations. The Storyboard Editor isn’t as powerful as Interface Builder yet, there are a few things IB can do that the Storyboard Editor unfortunately can’t. You also need a big monitor, especially when you write iPad apps!
If you’re the type who hates Interface Builder and who really wants to create his entire UI programmatically, then storyboards are probably not for you. Personally, I prefer to write as little code as possible — especially UI code! — so this tool is a welcome addition to my arsenal.
You can still use nibs with iOS 5 and Xcode 4.2. Using Interface Builder isn’t suddenly frowned upon now that we have storyboards. If you want to keep using nibs then go right ahead, but know that you can combine storyboards with nibs. It’s not an either-or situation.
In this tutorial we’ll take a look at what you can do with storyboards. The app we’re going to build is a bit pointless but it does show how to perform the most common tasks that you will be using storyboards for.
Getting Started
Fire up Xcode MonoDevelop and create a new project solution. We’ll use the iPhone Storyboard Single View Application template as our starting point and then build up the app from there.
Fill in the template options as follows:
ProductName: Ratings- Location: Folder that contains the solution folder
- Solution Name: Ratings
Company Identifier: the identifier that you use for your apps, in reverse domain notationClass Prefix: leave this emptyDevice Family: iPhoneUse Storyboard: check thisUse Automatic Reference Counting: check thisInclude Unit Tests: this should be unchecked
After Xcode MonoDevelop has created the project, the main Xcode MonoDevelop window looks like this:
Our new project consists of two classes, AppDelegate and RatingsViewController, and the star of this tutorial: the MainStoryboard.storyboard file. Notice that there are no .xib files in the project, not even MainWindow.xib.
Let’s take a look at that storyboard. Double-click the MainStoryboard.storyboard file in the Project Navigator Solution Pad to open the Xcode Storyboard Editor. MonoDevelop automatically creates this Xcode project that you will use to edit your storyboard.
The Storyboard Editor looks and works very much like Interface Builder. You can drag new controls from the Object Library (see bottom-right corner) into your view controller to design its layout. The difference is that the storyboard doesn’t contain just one view controller from your app, but all of them.
The official storyboard terminology is “scene”, but a scene is really nothing more than a view controller. Previously you would use a separate nib for each scene / view controller, but now they are all combined into a single storyboard.
On the iPhone only one of these scenes is visible at a time, but on the iPad you can show several at once, for example the master and detail panes in a split-view, or the content of a popover.
To get some feel for how the editor works, drag some controls into the blank view controller:
The sidebar on the left is the Document Outline:
In Interface Builder this area lists just the components from your nib but in the Storyboard Editor it shows the contents of all your view controllers. Currently there is only one view controller in our storyboard but in the course of this tutorial we’ll be adding several others.
There is a miniature version of this Document Outline below the scene, named the Dock:
The Dock shows the top-level objects in the scene. Each scene has at least a First Responder object and a View Controller object, but it can potentially have other top-level objects as well. More about that later. The Dock is convenient for making connections. If you need to connect something to the view controller, you can simply drag to its icon in the Dock.
Note: You probably won’t be using the First Responder very much. This is a proxy object that refers to whatever object has first responder status at any given time. It was also present in Interface Builder and you probably never had a need to use it then either. As an example, you could hook up the Touch Up Inside event from a button to First Responder’s cut: selector. If at some point a text field has input focus then you can press that button to make the text field, which is now the first responder, cut its text to the pasteboard.
Quit Xcode, run the app from MonoDevelop and it should look exactly like what we designed in the editor:
If you’ve ever made a nib-based app before then you always had a MainWindow.xib file. This nib contained the top-level UIWindow object, a reference to the App Delegate, and one or more view controllers. When you put your app’s UI in a storyboard, however, MainWindow.xib is no longer used.
So how does the storyboard get loaded by the app if there is no MainWindow.xib file?
Let’s take a peek at our application delegate. Open up AppDelegate.h AppDelegate.cs and you’ll see it looks like this:
[sourcecode language=”csharp”] using System; using System.Collections.Generic; using System.Linq;
using MonoTouch.Foundation; using MonoTouch.UIKit;
namespace Ratings { // The UIApplicationDelegate for the application. This class // is responsible for launching the User Interface of the // application, as well as listening (and optionally // responding) to application events from iOS. [Register (“AppDelegate”)] public partial class AppDelegate : UIApplicationDelegate { // class-level declarations
public override UIWindow Window { get; set; } [/sourcecode]
It is a requirement for using storyboards that your application delegate inherits from UIResponder (previously it used to inherit directly from NSObject) and that it has a UIWindow property (unlike before, this is not an IBOutlet). This is handled a little differently in MonoTouch because of how it implements Objective-C protocols.
If you look into further down in AppDelegate.m AppDelegate.cs, you’ll see that it does absolutely nothing, all the methods are practically empty. Even application:didFinishLaunchingWithOptions: FinishedLaunching simply returns YES true. Previously, this would either add the main view controller’s view to the window or set the window’s rootViewController property, but none of that happens here.
The secret is in the Info.plist file. Double-click on Ratings- Info.plist (it’s in the Supporting Files group project) and you’ll see this:
In nib-based projects there was a key in Info.plist named NSMainNibFile, or “Main nib file base name”, that instructed UIApplication to load MainWindow.xib and hook it into the app. Our Info.plist no longer has that setting.
Instead, storyboard apps use the key UIMainStoryboardFile, or “Main storyboard file base name”, to specify the name of the storyboard that must be loaded when the app starts. When this setting is present, UIApplication will load the MainStoryboard.storyboard file and automatically instantiates the first view controller from that storyboard and puts its view into a new UIWindow object. No programming necessary.
You can also see this in the Target Summary screen: The MonoDevelop iOS Application Target panel looks very much like the Xcode Target Summary screen:
There is a new iPhone/iPod Deployment Info section that lets you choose between starting from a storyboard or from a nib file. I don’t see any evidence that MonoDevelop let’s you choose betwen starting from a storyboard or from a nib file at this point. You may recall you had that choice when you created the project. (See Getting Started above.)
While we’ve got the Info.plist open, let’s fill in the iOS Application Target information:
- Application name: Ratings
- Identifier: identifier you use for your apps, in reverse domain notation
- Version: version number useful for your customers
- Devices: iPhone/iPod
- Deployment Target: 5.0
For the sake of completeness, also open main.m main.cs to see what’s in there:
[sourcecode language=”csharp”] using System; using System.Collections.Generic; using System.Linq;
using MonoTouch.Foundation; using MonoTouch.UIKit;
namespace Ratings { public class Application { // This is the main entry point of the application. static void Main (string[] args) { // if you want to use a different Application // Delegate class from “AppDelegate” you can // specify it here. UIApplication.Main (args, null, “AppDelegate”); } } } [/sourcecode]
Previously, the last parameter for UIApplicationMain() was nil but now it is NSStringFromClass([AppDelegate class]) “AppDelegate”.
A big difference with having a MainWindow.xib is that the app delegate is not part of the storyboard. Because the app delegate is no longer being loaded from a nib (nor from the storyboard), we have to tell UIApplicationMain specifically what the name of our app delegate class is, otherwise it won’t be able to find it.
(If you’d like to specify the class name more like the Xcode example, use typeof(AppDelegate).Name in place of NSStringFromClass([AppDelegate class]). Personally, I wouldn’t bother as I don’t see any value in “fixing” code MonoDevelop already generated correctly.)
Just Add It To My Tab
Our Ratings app has a tabbed interface with two screens. With a storyboard it is really easy to create tabs.
Switch back to Double-click MainStoryboard.storyboard to open the storyboard in Xcode, and drag a Tab Bar Controller from the Object Library into the canvas. You may want to maximize your Xcode window first, because the Tab Bar Controller comes with two view controllers attached and you’ll need some room to maneuver.
The new Tab Bar Controller comes pre-configured with two other view controllers, one for each tab. UITabBarController is a so-called container view controller because it contains one or more other view controllers. Two other common containers are the Navigation Controller and the Split View Controller (we’ll see both of them later). Another cool addition to iOS 5 is a new API for writing your own container controllers – and later on in this book, we have a tutorial on that! (The author is referring to the book, iOS 5 By Tutorials.)
The container relationship is represented in the Storyboard Editor by the arrows between the Tab Bar controller and the view controllers that it contains.
Note: If you want to move the Tab Bar controller and its attached view controllers as a group, you can Cmd-click to select multiple scenes and then move them around together. (Selected scenes have a thick blue outline.)
Drag a label into the first view controller and give it the text “First Tab”. Also drag a label into the second view controller and name it “Second Tab”. This allows us to actually see something happen when you switch between the tabs.
Note: You can’t drag stuff into the scenes when the editor is zoomed out. You’ll need to return to the normal zoom level first.
Select the Tab Bar Controller and go to the Attributes Inspector. Check the box that says Is Initial View Controller.
In the canvas the arrow that at first pointed to the regular view controller now points at the Tab Bar Controller:
This means that when you run the app, UIApplication will make the Tab Bar Controller the main screen of our app.
The storyboard always has a single view controller that is designated the initial view controller, that serves as the entry point into the storyboard.
Quit Xcode, switch back to MonoDevelop, run the app and try it out. The app now has a tab bar and you can switch between the two view controllers with the tabs:
Xcode MonoDevelop actually comes with a template for building a tabbed app (unsurprisingly called the Tabbed Application template) that we could have used, but it’s good to know how this works so you can also create one by hand if you have to.
You can remove the view controller that was originally added by the template as we’ll no longer be using it. The storyboard now contains just the tab bar and the two scenes for its tabs.
By the way, if you connect more than five scenes to the Tab Bar Controller, it automatically gets a More… tab when you run the app. Pretty neat!
Adding a Table View Controller
The two scenes that are currently attached to the Tab Bar Controller are both regular UIViewControllers. I want to replace the scene from the first tab with a UITableViewController instead.
Click on that first view controller to select it and then delete it. From the Object Library drag a new Table View Controller into the canvas in the place where that scene used to be:
With the Table View Controller selected, choose Editor\Embed In\Navigation Controller from Xcode’s menubar. This adds yet another view controller to the canvas:
You could also have dragged in a Navigation Controller from the Object Library, but this Embed In command is just as easy.
Because the Navigation Controller is also a container view controller (just like the Tab Bar Controller), it has a relationship arrow pointing at the Table View Controller. You can also see these relationships in the Document Outline:
Notice that embedding the Table View Controller gave it a navigation bar. The Storyboard Editor automatically put it there because this scene will now be displayed inside the Navigation Controller’s frame. It’s not a real UINavigationBar object but a simulated one.
If you look at the Attributes Inspector for the Table View Controller, you’ll see the Simulated metrics section at the top:
“Inferred” is the default setting for storyboards and it means the scene will show a navigation bar when it’s inside of a navigation controller, a tab bar when it’s inside of a tab bar controller, and so on. You could override these settings if you wanted to, but keep in mind they are here only to help you design your screens. The Simulated Metrics aren’t used during runtime, they’re just a visual design aid that shows what your screen will end up looking like.
Let’s connect these new scenes to our Tab Bar Controller. Ctrl-drag from the Tab Bar Controller to the Navigation Controller:
When you let go, a small popup menu appears:
Choose the Relationship – viewControllers option. This creates a new relationship arrow between the two scenes:
The Tab Bar Controller has two such relationships, one for each tab. The Navigation Controller itself has a relationship connection to the Table View Controller. There is also another type of arrow, the segue, that we’ll talk about later.
When we made this new connection, a new tab was added to the Tab Bar Controller, simply named “Item”. I want this new scene to be the first tab, so drag the tabs around to change their order:
Quit Xcode, run the app from MonoDevelop and try it out. The first tab now contains a table view inside a navigation controller.
Before we put some actual functionality into this app, let’s clean up the storyboard a little. I want to name the first tab “Players” and the second “Gestures”. You do not change this on the Tab Bar Controller itself, but in the view controllers that are connected to these tabs.
As soon as you connect a view controller to the Tab Bar Controller, it is given a Tab Bar Item object. You use the Tab Bar Item to configure the tab’s title and image.
Select the Tab Bar Item inside the Navigation Controller and in the Attributes Inspector set its Title to “Players”:
Rename the Tab Bar Item for the view controller from the second tab to “Gestures”. Quit Xcode and return to MonoDevelop.
We should also put some pictures on these tabs. The resources for this tutorial contains a subfolder named Images. Add that folder to the project in MonoDevelop by right-clicking the project and selecting Add→Add Existing Folder:
You might also want to add the images folder to the Xcode project so that you can see the images on the storyboard. Sadly, I don’t know how to do that without putting the images in the top level folder of the MonoDevelop project. I tried to add into Xcode a reference to the Images folder in the MonoDevelop project so you can select the images from the Xcode Attributes Inspector:
In the Attributes Inspector for the Players Tab Bar Item, choose the Players.png image. You probably guessed it, but give the Gestures item the image Gestures.png.
But it does not work in MonoDevelop. In Xcode, change Players.png to Images/Players.png in the Attributes Inspector for the Players Tab Bar Item. This should now working in MonoDevelop. Again, to fix this, copy the images into the top level of the MonoDevelop project, not within an Images folder. I prefer the little hassle in Xcode to having all these files visible at the top level in MonoDevelop. You may have a different preference.
If anyone knows how to fix this, please leave a comment below and I’ll update the article. Thanks.
Similarly, a view controller inside a Navigation Controller has a Navigation Item that is used to configure the navigation bar. (By now, you know enough to double-click the storyboard file in MonoDevelop, edit the storyboard, and quit Xcode to return to MonoDevelop to test, so I’ll stop the instructions about that.) Select the Navigation Item for the Table View Controller and change its title in the Attributes Inspector to “Players”.
Alternatively, you can simply double-click the navigation bar and change the title there. (Note: You should double-click the simulated navigation bar in the Table View Controller, not the actual Navigation Bar object in the Navigation Controller.)
Run the app and marvel at our pretty tab bar, all without writing a single line of code!
Prototype cells
You may have noticed that ever since we added the Table View Controller, Xcode has been complaining:
The warning message is, “Unsupported Configuration: Prototype table cells must have reuse identifiers”. When you add a Table View Controller to a storyboard, it wants to use prototype cells by default but we haven’t properly configured this yet, hence the warning. I don’t get this warning in MonoDevelop, but let’s learn about prototype cells because:
Prototype cells are one of the cool advantages that storyboards offer over regular nibs. Previously, if you wanted to use a table view cell with a custom design you either had to add your subviews to the cell programmatically, or create a new nib specifically for that cell and then load it from the nib with some magic. That’s still possible, but prototype cells make things a lot easier for you. Now you can design your cells directly in the storyboard editor.
The Table View Controller comes with a blank prototype cell. Click on that cell to select it and in the Attributes Inspector set Style to Subtitle. This immediately changes the appearance of the cell to include two labels. If you’ve used table views before and created your own cells by hand, you may recognize this as the UITableViewCellStyleSubtitle style. With prototype cells you can either pick one of the built-in cell styles as we just did, or create your own custom design (which we’ll do shortly).
Set the Accessory attribute to Disclosure Indicator and give the cell the Reuse Identifier “PlayerCell“. That will make Xcode shut up about the warning. All prototype cells are still regular UITableViewCell objects and therefore should have a reuse identifier. Xcode is just making sure we don’t forget (at least for those of us who pay attention to its warnings).
Run the app, and… nothing has changed. That’s not so strange, we still have to make a data source for the table so it will know what rows to display.
Add a new file to the project. Choose the UIViewController subclass template. Name the class PlayersViewController and make it a subclass of UITableViewController. The With XIB for user interface option should be unchecked because we already have the design of this view controller in the storyboard. No nibs today! We’re going to let MonoDevelop do it’s magic here and generate the PlayersViewController class for us.
Go back to the Storyboard Editor and select the Table View Controller. In the Identity Inspector, set its Class to PlayersViewController. That is the essential step for hooking up a scene from the storyboard with your own view controller subclass. Don’t forget this or your class won’t be used!
Quit Xcode and return to MonoDevelop. Wait a moment while MonoDevelop automatically generates the PlayersViewController class and adds it to your project.
[sourcecode language=”csharp”] // This file has been autogenerated from parsing an Objective-C header file added in Xcode.
using System;
using MonoTouch.Foundation; using MonoTouch.UIKit;
namespace Ratings { public partial class PlayersViewController : UITableViewController { public PlayersViewController (IntPtr handle) : base (handle) { } } } [/sourcecode]
From now on when you run the app that table view controller from the storyboard is actually an instance of our PlayersViewController class.
Add a new MonoTouch Empty Class to the project. Name it TableSource, subclass of UITableViewSource. The UITableViewSource class is a convenience class that combines the UITableViewDataSource and UITableViewDelegate protocols into a single class. Add a mutable array property private instance variable to PlayersViewController.h TableSource.cs:
[sourcecode language=”csharp”] using System; using System.Collections.Generic; using System.IO; using MonoTouch.Foundation; using MonoTouch.UIKit;
namespace Ratings { public class TableSource : UITableViewSource { private IList<Player> players;
public TableSource (IList<Player> players) { this.players = players; } } } [/sourcecode]
This array list will contain the main data model for our app. It contains Player objects. Let’s make that Player class right now. Add a new file to the project using the Objective-C MonoDevelop Empty Class template. Name it Player, subclass of NSObject.
Change Player.h Player.cs to the following:
[sourcecode language=”csharp”] using System;
namespace Ratings { public class Player { public String Name { get; set; } public String Game { get; set; } public int Rating { get; set; }
public Player () { } } } [/sourcecode]
There’s nothing special going on here. Player is simply a container object for these three properties: the name of the player, the game he’s playing, and a rating (1 to 5 stars).
We’ll make the array list and some test Player objects in our App Delegate and then assign it to the PlayersViewController TableSource’s players property.
In AppDelegate.m AppDelegate.cs, add an #import for the Player and PlayersViewController classes at the top of the file, and add a new instance variable named players:
[sourcecode language=”csharp”] private List<Player> players; [/sourcecode]
Then change the didFinishLaunchingWithOptions FinishedLaunching method to:
[sourcecode language=”csharp”] public override bool FinishedLaunching ( UIApplication application, NSDictionary launchOptions) { players = new List<Player>() { new Player() { Name = “Bill Evans”, Game = “Tic-Tac-Toe”, Rating = 4, }, new Player() { Name = @”Oscar Peterson”, Game = @”Spin the Bottle”, Rating = 5, }, new Player() { Name = @”Dave Brubeck”, Game = @”Texas Hold’em Poker”, Rating = 2, }, };
// Dig through storyboard to find PlayersViewController UITabBarController tbc = this.Window.RootViewController as UITabBarController; UINavigationController nc = tbc.ViewControllers[0] as UINavigationController; PlayersViewController pvc = nc.ViewControllers[0] as PlayersViewController;
pvc.TableView.Source = new TableSource(players);
return true; } [/sourcecode]
This simply creates some Player objects and adds them to the players array list. But then it does the following:
[sourcecode language=”csharp”] UITabBarController tbc = this.Window.RootViewController as UITabBarController; UINavigationController nc = tbc.ViewControllers[0] as UINavigationController; PlayersViewController pvc = nc.ViewControllers[0] as PlayersViewController; [/sourcecode]
Yikes, what is that?! We want to assign the players array to the players property of create an instance of TableSource as the data source for PlayersViewController so it can use this array for its data source. But the app delegate doesn’t know anything about PlayersViewController yet, so it will have to dig through the storyboard to find it.
This is one of the limitations of storyboards that I find annoying. With Interface Builder you always had a reference to the App Delegate in your MainWindow.xib and you could make connections from your top-level view controllers to outlets on the App Delegate. That is currently not possible with storyboards. You cannot make references to the app delegate from your top-level view controllers. That’s unfortunate, but we can always get those references programmatically.
[sourcecode language=”csharp”] UITabBarController tbc = this.Window.RootViewController as UITabBarController; [/sourcecode]
We know that the storyboard’s initial view controller is a Tab Bar Controller, so we can look up the window’s rootViewController and cast it.
The PlayersViewController sits inside a navigation controller in the first tab, so we look up that UINavigationController object:
[sourcecode language=”csharp”] UINavigationController nc = tbc.ViewControllers[0] as UINavigationController; [/sourcecode]
and then ask it for its root view controller, which is the PlayersViewController that we are looking for:
[sourcecode language=”csharp”] PlayersViewController pvc = nc.ViewControllers[0] as PlayersViewController; [/sourcecode]
Unfortunately, UINavigationController has no rootViewController property so we’ll have to delve into its viewControllers array. (It does have a topViewController property but that points to the top-most controller on the stack and we’re looking for the bottom-most one. At this point the app has just launched so technically we could have used topViewController, but that is not always the case.)
Now that we have an array full initialized our list of Player objects, we can continue building the data source for PlayersViewController.
Open up PlayersViewController.m, and change the table view data source methods to the following: Open up TableSource.cs and add the following table view data source methods:
[sourcecode language=”csharp”] public override int NumberOfSections (UITableView tableView) { return 1; }
public override int RowsInSection (UITableView tableview, int section) { return players.Count; } [/sourcecode]
The real work happens in cellForRowAtIndexPath. The version from the Xcode template looks like this:
| | | — | |
- (UITableViewCell \*)tableView:(UITableView \*)tableView
cellForRowAtIndexPath:([NSIndexPath](http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSIndexPath_Class/) \*)indexPath
{
static [NSString](http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/) \*CellIdentifier = @"Cell";
UITableViewCell \*cell = [tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
// Configure the cell...
return cell;
}
|
That is no doubt how you’ve been writing your own table view code all this time. Well, no longer! Replace that method with:
[sourcecode language=”csharp”] public override UITableViewCell GetCell ( UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath) { UITableViewCell cell = tableView.DequeueReusableCell (“PlayerCell”) as UITableViewCell;
Player player = players[indexPath.Row]; cell.TextLabel.Text = player.Name; cell.DetailTextLabel.Text = player.Game; return cell; } [/sourcecode]
That looks a lot simpler! The only thing you need to do to get a new cell is:
[sourcecode language=”csharp”] UITableViewCell cell = tableView.DequeueReusableCell (“PlayerCell”) as UITableViewCell; [/sourcecode]
If there is no existing cell that can be recycled, this will automatically make a new copy of the prototype cell and return it to you. All you need to do is supply the reuse identifier that you set on the prototype cell in the storyboard editor, in our case “PlayerCell“. Don’t forget to set that identifier, or this little scheme won’t work!
Because this class doesn’t know anything about the Player object yet, it needs an #import at the top of the file:
And we should not forget to synthesize the property that we added earlier:
Now you can run the app, and lo and behold, the table view has players in it:
Note: In this app we’re using only one prototype cell but if your table needs to display different kinds of cells then you can simply add additional prototype cells to the storyboard. You can either duplicate the existing cell to make a new one, or increment the value of the Table View’s Prototype Cells attribute. Be sure to give each cell its own re-use identifier, though.
It just takes one line of code to use these newfangled prototype cells. I think that’s just great!
Designing Our Own Prototype Cells
Using a standard cell style is OK for many apps, but I want to add an image on the right-hand side of the cell that shows the player’s rating (in stars). Having an image view in that spot is not supported by the standard cell styles, so we’ll have to make a custom design.
Switch back to MainStoryboard.storyboard, select the prototype cell in the table view, and set its Style attribute to Custom. The default labels now disappear.
First make the cell a little taller. Either drag its handle at the bottom or change the Row Height value in the Size Inspector. I used the latter method to make the cell 55 points high. And I used the former for MonoTouch because it seemed easier.
Drag two Label objects from the Objects Library into the cell and place them roughly where the labels were previously. Just play with the font and colors and pick something you like. Do set the Highlighted color of both labels to white. That will look better when the user taps the cell and the cell background turns blue.
Drag an Image View into the cell and place it on the right, next to the disclosure indicator. Make it 81 points wide, the height isn’t very important. Set its Mode to Center (under View in the Attributes Inspector) so that whatever image we put into this view is not stretched.
I made the labels 210 points wide so they don’t overlap with the image view. The final design for the prototype cell looks something like this:
Because this is a custom designed cell, we can no longer use UITableViewCell’s textLabel and detailTextLabel properties to put text into the labels. These properties refer to labels that aren’t on our cell anymore. Instead, we will use tags to find the labels.
Give the Name label tag 100, the Game label tag 101, and the Image View tag 102. You can do this in the Attributes Inspector.
Then open PlayersViewController.m TableSource.cs and change cellForRowAtIndexPath from PlayersViewController GetCell from TableSource to:
[sourcecode language=”csharp”] public override UITableViewCell GetCell ( UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath) { UITableViewCell cell = tableView.DequeueReusableCell (“PlayerCell”) as UITableViewCell;
Player player = players[indexPath.Row];
UILabel nameLabel = cell.ViewWithTag(100) as UILabel; nameLabel.Text = player.Name;
UILabel gameLabel = cell.ViewWithTag(101) as UILabel; gameLabel.Text = player.Game;
UIImageView ratingImageView = cell.ViewWithTag(102) as UIImageView; ratingImageView.Image = this.ImageForRating(player.Rating);
return cell; } [/sourcecode]
This uses a new method, ImageForRating. Add that method above (or below – this is C#!) cellForRowAtIndexPath GetCell:
[sourcecode language=”csharp”] private UIImage ImageForRating(int rating) { switch (rating) { case 1: return UIImage.FromFile(“Images/1StarSmall.png”); case 2: return UIImage.FromFile(“Images/2StarsSmall.png”); case 3: return UIImage.FromFile(“Images/3StarsSmall.png”); case 4: return UIImage.FromFile(“Images/4StarsSmall.png”); case 5: return UIImage.FromFile(“Images/5StarsSmall.png”); } return null; } [/sourcecode]
That should do it. Now run the app again.
Hmm, that doesn’t look quite right. We did change the height of the prototype cell but the table view doesn’t automatically take that into consideration. There are two ways to fix it: we can change the table view’s Row Height attribute or implement the heightForRowAtIndexPath method. The former is much easier, so let’s do that.
Note: You would use heightForRowAtIndexPath if you did not know the height of your cells in advance, or if different rows can have different heights.
Back in MainStoryboard.storyboard, in the Size Inspector of the Table View, set Row Height to 55:
By the way, if you changed the height of the cell by dragging its handle rather than typing in the value, then the table view’s Row Height property was automatically changed too. So it may have worked correctly for you the first time around.
If you run the app now, it looks a lot better!
Using a Subclass for the Prototype Cell
Our table view already works pretty well but I’m not a big fan of using tags to access the labels and other subviews of the prototype cell. It would be much more handy if we could connect these labels to outlets and then use the corresponding properties. As it turns out, we can.
Add a new file to the project, with the Objective-C class template. Name it PlayerCell and make it a subclass of UITableViewCell.
Back in MainStoryboard.storyboard, select the prototype cell and change its Class to “PlayerCell“ on the Identity Inspector. Xcode automatically adds the class PlayerCell as a subclass of UITableViewCell. Now whenever you ask the table view for a new cell with dequeueReusableCellWithIdentifier, it returns a PlayerCell instance instead of a regular UITableViewCell.
It would be great if you could immediately add the outlets, but I couldn’t get Xcode to recognize the existence of the new PlayerCell class if I added them now. So quit Xcode, go back to MonoDevelop, and wait for MonoDevelop to detect the changes in Xcode and automatically add the new class, PlayerCell. Then restart Xcode and make the following changes to PlayerCell in Xcode:
Change PlayerCell.h to:
| | | — | |
@interface PlayerCell : UITableViewCell
@property (nonatomic, strong) IBOutlet UILabel \*nameLabel;
@property (nonatomic, strong) IBOutlet UILabel \*gameLabel;
@property (nonatomic, strong) IBOutlet UIImageView
\*ratingImageView;
@end
|
Replace the contents of PlayerCell.m with:
| | | — | |
#import "PlayerCell.h"
@implementation PlayerCell
@synthesize nameLabel;
@synthesize gameLabel;
@synthesize ratingImageView;
@end
|
The class itself doesn’t do much, it just adds properties for nameLabel, gameLabel and ratingImageView.
Note that I gave this class the same name as the reuse identifier — they’re both called PlayerCell — but that’s only because I like to keep things consistent. The class name and reuse identifier have nothing to do with each other, so you could name them differently if you wanted to.
Now you can connect the labels and the image view to these outlets. Either select the label and drag from its Connections Inspector to the table view cell, or do it the other way around, ctrl-drag from the table view cell back to the label:
Important: You should hook up the controls to the table view cell, not to the view controller! You see, whenever your data source asks the table view for a new cell with dequeueReusableCellWithIdentifier, the table view doesn’t give you the actual prototype cell but a *copy* (or one of the previous cells is recycled if possible). This means there will be more than one instance of PlayerCell at any given time. If you were to connect a label from the cell to an outlet on the view controller, then several copies of the label will try to use the same outlet. That’s just asking for trouble. (On the other hand, connecting the prototype cell to actions on the view controller is perfectly fine. You would do that if you had custom buttons or other UIControls on your cell.)
Now that we’ve hooked up the properties, we can quit Xcode, return to MonoDeveop, and simplify our data source code one more time.
[sourcecode language=”csharp”] public override UITableViewCell GetCell ( UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath) { PlayerCell cell = tableView.DequeueReusableCell (“PlayerCell”) as PlayerCell;
Player player = players[indexPath.Row]; cell.nameLabel.Text = player.Name; cell.gameLabel.Text = player.Game; cell.ratingImageView.Image = this.ImageForRating(player.Rating);
return cell; } [/sourcecode]
That’s more like it. We now cast the object that we receive from dequeueReusableCellWithIdentifier to a PlayerCell, and then we can simply use the properties that are wired up to the labels and the image view. I really like how using prototype cells makes table views a whole lot less messy!
You’ll need to import the PlayerCell class to make this work:
Run the app and try it out. Oops, it doesn’t build. Edit PlayerCell.designer.cs. Make nameLabel, gameLabel, and ratingImageView public. Now build and run. When you run the app it should still look the same as before, but behind the scenes we’re now using our own table view cell subclass!
Here are some free design tips. There are a couple of things you need to take care of when you design your own table view cells. First off, you should set the highlighted color of the labels so that they look good then the user taps the row:
Second, you should make sure that the content you add is flexible so that when the table view cell resizes, the content sizes along with it. Cells will resize when you add the ability to delete or move rows, for example.
Add the following method to PlayersViewController.m TableSource.cs:
[sourcecode language=”csharp”] public override void CommitEditingStyle ( UITableView tableView, UITableViewCellEditingStyle editingStyle, NSIndexPath indexPath) { if (editingStyle == UITableViewCellEditingStyle.Delete) { this.players.RemoveAt(indexPath.Row); tableView.DeleteRows(new NSIndexPath[] { indexPath }, UITableViewRowAnimation.Fade); } } [/sourcecode]
When this method is present, swipe-to-delete is enabled on the table. Run the app and swipe a row to see what happens.
The Delete button slides into the cell but partially overlaps the stars image. What actually happens is that the cell resizes to make room for the Delete button, but the image view doesn’t follow along.
To fix this, open MainStoryBoard.storyboard, select the image view in the table view cell, and in the Size Inspector change the Autosizing so it sticks to its superview’s right edge:
Autosizing for the labels should be set up as follows, so they’ll shrink when the cell shrinks:
With those changes, the Delete button appears to push aside the stars:
You could also make the stars disappear altogether to make room for the Delete button, but that’s left as an exercise for the reader. The important point is that you should keep these details in mind when you design your own table view cells!
Where To Go From Here?
Check out part two of this tutorial, where we’ll cover segues, static table view cells, the add player screen, a game picker screen, and the downloadable example project for this tutorial! Presently, part two has not been adapted for MonoTouch. That, too, is left as an exercise for the reader.
This “Beginning Storyboards in iOS 5″ series is one of the chapters in our new iOS 5 By Tutorials book. If you like what you see here, check out the book – there’s an entire second chapter on intermediate storyboarding, above and beyond what we’re posting for free here! :]
If you felt lost at any point during this tutorial, you also might want to brush up on the basics with my newly updated iOS Apprentice series. In that series, I cover the foundational knowledge you need as an iOS developer from the ground up – perfect for complete beginners, or those looking to fill in some gaps. To brush up on MonoTouch and MonoDevelop, see the many tutorials, samples, and recipes at Xamarin.
If you have any questions or comments on this tutorial or on storyboarding in iOS 5 in general, please join the forum discussion below! For MonoTouch and MonoDevelop, try Stack Overflow or the Xamarin iOS forums.
This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer.
This iOS Tutorial post was adapted for MonoTouch by Terry Westley, a not so experienced iOS developer.