Simplified User Interfaces on the iPhone with MonoTouch.Dialog

by Miguel de Icaza

At the core of the iPhone UI lies the UITableView, a powerful table rendering widget that is used by pretty much every application on the iPhone. The UITableView is a powerful widget that can render its data in many different ways based on how you configure the widget, these are all variations of the UITableView:

This is one powerful widget.

The contents of the UITableView are rendered by calling into a developer supplied set of routines that provide the data on demand. The protocol includes queries like "How many sections?", "How many rows in section N?", "What is the title for section N?" as well as callbacks to provide the actual contents of a cell. Although the widget is pretty powerful, creating UIs with it is a chore and a pain to maintain. The pain to maintain and the repetitive nature of the process leads to developers either spending too much time customizing each view, bare minimum configuration and lack of polish on certain configurations. As I ported many of the Objective-C samples to C# I found myself repeating the same process over and over.

My fingers developed calluses, and at night I kept thinking that there should be a better way. But at the time, I was just doing a line-by-line port, I was not really ready to build a new API on top of it.

Recently, when my favorite twitter client on the iPhone butchered its UI I figured I would write my own Twitter client. The first step was to create the account configuration for my twitter account. As you can imagine, the configuration is done with a variation of the UITableView. And once again I found myself setting up the model, responding to view events, sprinkling switch and if statements liberally all through my source code and just plainly not having any fun writing the code. This is how MonoTouch.Dialog was born.

I wanted to reflection to allow a class to be mapped to a dialog box, something that would allow me to write a C# class and have it mapped to a UITableView, something like this:

class TwitterConfig {
    [Section ("Account")]
      [Entry] string Username;
      [Password] string Password;

   [Section ("Settings")]
      bool AutoRefresh;
      bool AutoLoad;
      bool UseTwitterRetweet;
}

Instead of starting with the Reflection code to deal with this, I first created an in memory representation of the entire dialog. The idea was that the Reflection code would then be a bridge that could use the engine code.

The engine code is built on the idea that each row could be a specific kind of widget. It could contain text, a switch box, a text editing line, a slider control, a calendar picker or any other kind of user created control. I call these "Elements" and I created a bunch of these:

  • BooleanElement: rendered with a UISwitch
  • FloatElement: rendered with a slider.
  • HtmlElement: when tapped, starts the web browser on a url.
  • StringElement: used to render plain strings.
  • MultilineElement: A multi-line string element.
  • RadioElement: a radio-item, to select one of many choices.
  • CheckboxElement: like the BooleanElement, but uses a checkbox instead of a UISwitch.
  • ImageElement: Allows the user to pick an image or take a photo.
  • EntryElement: text entry element.
  • DateTimeElement, DateElement, TimeElement: pickers for dates and times, dates and times.

The MonoTouch.Dialog follows the Apple HIG for the iPhone and implements as much of the UI policy allowing me to focus on the actual application instead of focusing on the administrivia.

Although the UITableView is built on the powerful Model/View/Controller setup that would allow it to scale efficiently to large data sets, most configuration pages and data entry pages do not require this complexity.

Another feature is that it takes care of all the bookkeeping required to do text entry without any work from the programmer: accepting keyboard input, automatically switching to the next entry line on return, aligning all entry lines in a section, dismissing the keyboard with the last input is reached.

Here is a sample of the API in action:

var root = new RootElement ("Settings") {
  new Section (){
    new BooleanElement ("Airplane Mode", false),
  new RootElement ("Notifications", 0, 0) { Notifications }
  new Section (){
    new RootElement ("Sound"), { Sound },
    new RootElement ("Brightness"){ Brightness },
    new RootElement ("Wallpaper"){ Wallpaper }
  },
  new Section () {
    new EntryElement ("Login", "Your login name", "miguel"),
    new EntryElement ("Password", "Your password", "password", true),
    new DateElement ("Select Date", DateTime.Now),
    new TimeElement ("Select Time", DateTime.Now),
  }
}
	

Once the RootElement has been created, this can be passed to a DialogViewController to manage it:

var dv = new DialogViewController (root);
navigation.PushViewController (dv, true);

Reflection API

The reflection API inspects a class and looks for fields that have some special attributes on them.

Here is a sample class and how it is rendered:

class AccountInfo {
    [Section]
    public bool AirplaneMode;

    [Section ("Data Entry", "Your credentials")]

    [Entry ("Enter your login name")]
    public string Login;

    [Caption ("Password"), Password ("Enter your password")]
    public string passwd;

    [Section ("Travel options")]
    public SeatPreference preference;
}
	

As you can see, enumerations (SeatPreference) are automatically turned into a radio selection that uses the UINavigationController to navigate and the captions are computed from the field names, a behavior that you can customize with the [Caption] attribute.

The attributes that are attached to each instance variable control how things are rendered and can be used to specify a variety of attributes like captions, images and overriding the defaults from MonoTouch.Dialog.

Some Samples

Code:

new RootElement ("Settings") {
  new Section (){
    new BooleanElement ("Airplane Mode", false),
    new RootElement ("Notifications", 0, 0) { Notifications }
  new Section (){
    new RootElement ("Sound"), { Sound },
    new RootElement ("Brightness"){ Brightness },
    new RootElement ("Wallpaper"){ Wallpaper }
  },
  new Section () {
    new EntryElement ("Login", "Your login name", "miguel"),
    new EntryElement ("Password", "Your password", "password", true),
    new DateElement ("Select Date", DateTime.Now),
    new TimeElement ("Select Time", DateTime.Now),
  }
}
	

LINQ and MonoTouch.Dialog

Craig has written a great Conference application for the Mix 2010 conference. I helped him reduce the code size of the application by removing all of the repetitive code required to set up UITableViews for various pieces of the application with MonoTouch.Dialog. Since the conference application deals with a database schedule, I extended MonoTouch.Dialog to work better with LINQ.

In the same spirit of the System.Xml.Linq API that allows you to create XML documents with nested LINQ statements, you can use MonoTouch.Dialog to create your entire UIs.

For Craig's application, I wrote a SessionElement that allows sessions to be "starred" and shows both the title of the session as well as the location of the session.

This code constructs the entire UI for the "My Schedule" tab. The data is populated on demand (Apple recommends that all views are loaded lazily)

public class FavoritesViewController : DialogViewController {
  public FavoritesViewController () : base (null) { }

  public override void ViewWillAppear (bool animated)
  {
    var favs = AppDelegate.UserData.GetFavoriteCodes();
    Root = new RootElement ("Favorites") {
      from s in AppDelegate.ConferenceData.Sessions
        where favs.Contains(s.Code)
        group s by s.Start into g
        orderby g.Key
        select new Section (MakeCaption ("", g.Key)) {
          from hs in g
            select (Element) new SessionElement (hs)
        }
    };
  }
}
	

That is it. The entire source code.

So use any of the two models that you enjoy the most: Reflection for quick one-offs and quick data-entry or the Element API for more advanced user interfaces but without having to spend your life writing boilerplate code.

I hope that this helps guys spend more time improving their applications, and less time writing boilerplate code.

MonoTouch.Dialog is not perfect and does not have every possible bell and whistle that you might want to add. Although I do welcome patches for certain features, feel free to fork the code and patch at will to suit your needs.

Posted on 23 Feb 2010