Using a UISplitViewController to Create a Master-Detail iPad App with MonoTouch

The UISplitViewController is a new controller that is designed for the iPad to create and manage two separate views (and their controllers) side-by-side of each other. This lends itself well to a master-detail scenario, where the master list in kept on the left and the detail view fills the remaining space on the right, when in landscape orientation. In portrait the left-hand view is hidden and can be presented using the new UIPopoverController in coordination with the UISplitViewControllerDelegate. I’ll touch on some of the details of this here and then provide a basic working sample to get you started.

In MonoDevelop you can create an iPad app using the iPad Window-based project template. After creating a new app, open up MainWindow.xib and add a UISplitViewController. The default UISplitViewController adds UINavigationController containing a UITableViewController on the left side of the split view and a UIViewController on the right. The table will contain the master list. When you select a row, the detail view will update on the right.

Programming the table is identical to the way you do it on the iPhone. Add a class to the project in MonoDevelop with all the code for a UITableViewController subclass (decorated with the Register attribute ) and then in Interface Builder, you can set the class of the UITableViewController in the Identity Inspector as usual (I named it MasterTableViewController). For the detail view, we’ll load it from a separate xib. Create a new View Interface with Controller called DetailViewController and then, back in Interface Builder, you can set the class of the right-hand view controller to DetailViewController and the nib name to DetailViewController. The default template for the DetailViewController.xib will be that of an iPhone at the time of this writing. To make it look like an iPad at design-time, before adding anything to the contained view, you can use File>Create iPad version in Interface Builder to create a copy with the iPad template and overwite the existing iPhone one. Then you can open the new xib in Interface Builder and the designer will be sized for an iPad. I added a label to the detail view which I updated with the text of the row selected in the table along with the outlet for it in the DetailViewController. Also in the DetailViewController’s view, I added a UINavgationBar to the top, also with an outlet.

Here’s what Interface Builder looks like for the MainWindow.xib when complete:

and here is the DetailViewController.xib (there is a label UILabel in the center of the View with the text removed):

When you rotate between landscape and portrait orientations, the left (master) view will be shown and hidden respectively. A button is added to the UINavigationBar in portrait orientation in order to present the master view in the new UIPopoverController. The button and the UIPopoverController are both passed into the UISplitViewControllerDelegate when the left view will be hidden (when entering portrait). As the orientation changes you can manage adding and removing the
button and the existence of the popover by subclassing WillHideViewController and WillShowViewController.


public class SplitDelegate : UISplitViewControllerDelegate
{

public override void WillHideViewController (UISplitViewController svc, UIViewController aViewController, UIBarButtonItem barButtonItem, UIPopoverController pc)
{
DetailViewController dvc = svc.ViewControllers[1] as DetailViewController;

if (dvc != null) {
dvc.AddNavBarButton (barButtonItem);
dvc.Popover = pc;
}
}

public override void WillShowViewController (UISplitViewController svc, UIViewController aViewController, UIBarButtonItem button)
{
DetailViewController dvc = svc.ViewControllers[1] as DetailViewController;

if (dvc != null) {
dvc.RemoveNavBarButton ();
dvc.Popover = null;
}
}

}

The DetailViewController has the implementation to add and remove the button:


...
public void AddNavBarButton (UIBarButtonItem button)
{
button.Title = "Master List";
navBar.TopItem.SetLeftBarButtonItem (button, false);
}

public void RemoveNavBarButton ()
{
navBar.TopItem.SetLeftBarButtonItem (null, false);
}

The popover is dismissed when you touch outside its area. I also programatically dismiss it after a cell is selected (when in portait orientation) within the DetailViewController’s implementation when the detail data (a string in this simple example) is set.


public partial class DetailViewController : UIViewController
{
UIPopoverController _pc;
string _detail;

public UIPopoverController Popover {
get { return _pc; }
set { _pc = value; }
}

public string Detail {
get { return _detail; }
set {
_detail = value;

if (_pc != null) {
_pc.Dismiss (true);
}

detailLabel.Text = _detail;
}
}
...

One final note, you need to override ShouldAutorotateToInterfaceOrientation and return true for both the DetailViewController and the MasterViewController so that the UISplitViewController can make orientation changes to the ui appropriately, even thought the MasterViewController is not displayed in portrait mode other than in the popover.

The final running app looks like this:

The code for the sample project is available here:

iBabySmash

After I posted my previous how to on core graphics, someone asked me if I was porting Baby Smash to the iPhone. Well, I wasn’t at the time, but I had a rare couple of hours free this evening, so I decided to take a shot at it. The example shows how to generate various geometric shapes from primatives as well as how to fill with colors and images. Possible enhancements might include sound effects, free form drawing and animation. I’ll add those when I get a chance.

The code is available at http://github.com/mikebluestein/iBabySmash

Drawing with CoreGraphics in MonoTouch

CoreGraphics allows you to do custom drawing on the iPhone. MonoTouch exposes this in the namespace MonoTouch.CoreGraphics. This allows you to do all sorts of custom drawing. Here I’ll present an introductory example that shows how to draw a triangle in a custom view along with determining if the triangle was touched. Even as drawing complexity increases, you’ll find the code to be quite similar in general, just that there will be more of it.

For the example I created a new MonoTouch window-based app and added a new ViewController from the ViewController with View template, naming it DrawingViewController.xib. I then added a class called DrawingView, subclassed from UIView, that I wired up in Interface Builder to be the view under DrawingViewController. After that I decorated the DrawingView class with the Resigister attribute so it will be available for the Objective-C runtime. As it’s being created from the xib, I also included the constructor that takes an IntPtr. See some of the tutorials on the monotouch wiki or the articles aggregated at monotouch.info if you need to review the details of how to wire things up.

With the DrawingView class in place, you can override the Draw (RectangleF rect) method to implement custom drawing. You implement what you want to draw in the Draw method and CocoaTouch will call it at runtime during event loop processing. To draw you need to:

  1. Get a graphics context
  2. Set up drawing attributes
  3. Create some geometry from drawing primitives
  4. Call a Draw or Stroke method

Here’s a simple example that draws a rectangle and fills it with a color:

using System;
using MonoTouch.UIKit;
using System.Drawing;
using MonoTouch.CoreGraphics;
using MonoTouch.Foundation;

namespace CoreGraphicsDemo
{


	[Register("DrawingView")]
	public class DrawingView : UIView
	{
		CGPath path;
		
		public DrawingView (IntPtr p) : base(p)
		{
		}
			
		public override void Draw (RectangleF rect)
		{
			base.Draw (rect);
			
			//get graphics context
			CGContext gctx = UIGraphics.GetCurrentContext ();
			
			//set up drawing attributes
			gctx.SetLineWidth(4);
			UIColor.Purple.SetFill ();
			UIColor.Black.SetStroke ();
		
			//create geometry
		    path = new CGPath ();
			
			path.AddLines(new PointF[]{
				new PointF(100,200),
				new PointF(160,100), 
				new PointF(220,200)});
			
			path.CloseSubpath();
			
			//add geometry to graphics context and draw it
			gctx.AddPath(path);		
			gctx.DrawPath(CGPathDrawingMode.FillStroke);	
		}
        ...

To respond to touches on the triangle you can override TouchesBegan and make use of the LocationInView method on UITouch like this:

		public override void TouchesBegan (NSSet touches, UIEvent evt)
		{
			base.TouchesBegan (touches, evt);
			
			UITouch touch = touches.AnyObject as UITouch;
			
		    if(touch != null)
			{
				PointF pt = touch.LocationInView(this);
				
				if(path.ContainsPoint(pt, true))
				{
					Console.WriteLine("Hit {0}", pt.ToString());
				}
				else
				{
					Console.WriteLine("No Hit {0}", pt.ToString());
				}
			}  
		}

Sending Mail from an iPhone App with Monotouch

You can send email on the iPhone from within your application using the MFMailComposeViewController in the MonoTouch.MessageUI namespace. Simply check if the device is able to send mail using the CanSendMail property and if it is true, bring up an MFMailComposeViewController. The controller has properties to add attachments (using the AddAttachmentData method), set the message body, send html mail, add cc recipients, etc. After hydrating your controller and presenting it, you can listen for completion results by subscribing to the Finished event (or you could do it the Delegate way and override the Finished virtual function on MFMailComposeViewControllerDelegate as well). In the callback you get back a result object, an error object and the controller itself via the MFComposeResultEventArgs, which you can use to present the completion status and dismiss the controller. Here’s a simple example:

        MFMailComposeViewController _mail;
        ...

        public override void ViewDidLoad ()
        {
            base.ViewDidLoad ();
            
            mailButton.TouchUpInside += (o, e) =>
            {
                if (MFMailComposeViewController.CanSendMail) {
                    _mail = new MFMailComposeViewController ();
                    _mail.SetMessageBody ("This is the body of the email", 
                        false);
                    _mail.Finished += HandleMailFinished;
                    
                    this.PresentModalViewController (_mail, true);
                    
                } else {
                    // handle not being able to send mail
                }
            };
        }

        void HandleMailFinished (object sender, MFComposeResultEventArgs e)
        {
            if (e.Result == MFMailComposeResult.Sent) {
                UIAlertView alert = new UIAlertView ("Mail Alert", "Mail Sent",
                    null, "Yippie", null);
                alert.Show ();
                
                // you should handle other values that could be returned 
                // in e.Result and also in e.Error 
            }
            e.Controller.DismissModalViewControllerAnimated (true);
        }

Update: Added link to the sample project here.

A Consumer Product Recall Application – Data.gov with Monotouch

After watching the PDC keynote earlier today and listening to Vivek Kundra speak about that plethora of government data sets that are available, I decided to check out www.data.gov. One thing that caught my eye was the product recall data from the US Consumer Product Safety Commission. I figured it would be useful to have on my phone, especially with the holidays coming up, so I decided to build a quick app for it. The code is based upon my earlier fast scrolling post where I ported the Tweetie fast scrolling pattern to Monotouch. Otherwise, it’s a pretty straight-forward tab controller application. It shows product recalls sorted by date across several categories.

    

The tab bar images are from Joesph Wain at glyphish.com. Also thanks to Alex York for his excellent rss reader post.

You can download the code for the app here: http://cid-0486030770596f62.skydrive.live.com/self.aspx/Public/ConsumerRecalls.zip

Using MPMusicPlayerController and MPMediaPickerController from Monotouch to play music from your library

Using the handy MPMusicPlayerController and MPMediaPickerController, you can interact with your iPod library on your iPhone. You can select and play music (podcasts, etc) using these classes and the associated MPMediaPickerControllerDelegate without leaving your application (actually the music plays in the background just like the iPod app). Here’s a short demo app that will open the MPMediaPickerController modally to allow selection of a song. The song is queued in the MPMusicPlayerController when it is selected. For simplicity I added 2 buttons, 1 to open the controller and another to play whatever you selected. This only works on the device. Here’s the code for this small “app.” (Note: this example requires a patch that will be in monotouch 1.4).

using MonoTouch.Foundation;
using MonoTouch.UIKit;
using MonoTouch.MediaPlayer;

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

public partial class AppDelegate : UIApplicationDelegate
{
MPMusicPlayerController _musicPlayer;

public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
_musicPlayer = new MPMusicPlayerController ();

window.AddSubview (viewController.View);

iPodButton.TouchUpInside += (o, e) =>
{
MPMediaPickerController mediaController = new MPMediaPickerController (MPMediaType.MPMediaTypeMusic);
mediaController.AllowsPickingMultipleItems = false;
mediaController.Delegate = new MediaPickerDelegate (_musicPlayer, viewController);
viewController.PresentModalViewController (mediaController, true);
};

playButton.TouchUpInside += (o, e) =>
{
_musicPlayer.Stop ();
_musicPlayer.Play ();
};

window.MakeKeyAndVisible ();

return true;
}

public override void OnActivated (UIApplication application)
{
}
}

public class MediaPickerDelegate : MPMediaPickerControllerDelegate
{
MPMusicPlayerController _musicPlayer;
UIViewController _viewController;

public MediaPickerDelegate (MPMusicPlayerController musicPlayer, UIViewController viewController) : base()
{
_musicPlayer = musicPlayer;
_viewController = viewController;
}

public override void MediaItemsPicked (MPMediaPickerController sender, MPMediaItemCollection mediaItemCollection)
{
_musicPlayer.SetQueue (mediaItemCollection);
_viewController.DismissModalViewControllerAnimated (true);
}

public override void MediaPickerDidCancel (MPMediaPickerController sender)
{
_viewController.DismissModalViewControllerAnimated (true);
}
}

Fast Scrolling a UITableView in Monotouch

This is a port I did of the fast scrolling example here: http://blog.atebits.com/2008/12/fast-scrolling-in-tweetie-with-uitableview/

The main thing to note is the code in CustomTableViewCell.DrawContentView, which takes care of drawing the value set in the NSString Text property to the screen, as the cells come into view, while the table is scrolled.

public override void DrawContentView (RectangleF rect)
{
CGContext context = UIGraphics.GetCurrentContext ();

UIColor backgroundColor = UIColor.White;
UIColor textColor = UIColor.Black;

if (this.Highlighted) {
backgroundColor = UIColor.Clear;
textColor = UIColor.White;
}

backgroundColor.SetColor ();

context.FillRect (rect);

Point pt = new Point (50, 10);

textColor.SetColor ();

DrawString (Text, pt, _textFont);
}
}

This gets invoked via an abstract base class that derives from UITableViewCell. Actually, the base class nests another class, which subclasses UIView, that serves as the content container. It’s in that classes’ override of the Draw method that the call to DrawContentView happens.

public abstract class TableViewCellBase : UITableViewCell
{
TableViewCellContent _contentView;

public TableViewCellBase (string reuseIdentifier) : base(UITableViewCellStyle.Default, reuseIdentifier)
{
_contentView = new TableViewCellContent ();
this.Opaque = true;
this.AddSubview (_contentView);
}

public override RectangleF Frame {
get { return base.Frame; }
set {
base.Frame = value;

if (_contentView != null) {
RectangleF boundingRectangle = this.Bounds;
_contentView.Frame = boundingRectangle;
}

}
}

public override void SetNeedsDisplay ()
{
base.SetNeedsDisplay ();
if (_contentView != null)
_contentView.SetNeedsDisplay ();
}

public abstract void DrawContentView (RectangleF rect);

class TableViewCellContent : UIView
{
public TableViewCellContent () : base(RectangleF.Empty)
{
}

public override void Draw (RectangleF rect)
{
((TableViewCellBase)this.Superview).DrawContentView (rect);
}
}

}

I tried it on an iPhone 3G and the scrolling performance appeared to be very good.

Here’s the full project: http://cid-0486030770596f62.skydrive.live.com/self.aspx/Public/FastScrolling.zip

A Quick Look at the Monotouch iPhone Debugger

The new Monotouch iPhone debugger supports debugging on both the simulator and the device. The device debugging works over wifi, so once you’ve deployed the app to the device, you can debug it wirelessly when on the same network (how cool is that)! Here’s a quick look at the debugging experience:

In Monodevelop you just need to select the Debug|iPhone configuration (or Debug|Simulator if not using the device), build and deploy the debug build to the device as normal. Then, either connected or not, you can simply start debugging. When the debugger launches it will prompt you to start the app on the device. After startup, the interaction goes back and forth between the device and Monodevelop, stopping wherever you set breakpoints. So conceivably, you could have testers interacting with the app at their desks while you debug it at yours. Also note that standard output goes to the Application pad in Monodevelop. You can read more about the debugger here:  http://monotouch.net/Documentation/Upcoming/Debugger

Create a mini web server using Monotouch to serve up a Silverlight app to your desktop

Creating a minimalist web server running inside your app on an iPhone offers some interesting scenarios for sharing data on your local network. You can use this to serve up html, but I thought it would be interesting to have it serve up a Silverlight app. Since Silverlight apps are packaged in zip files (with a different extension, .xap) they make for a clean deployment in an iphone app scenario. You can create the server in your app using Systems.Net.Sockets. For example you can create a class that uses TcpListener to accept incoming traffic and then use TcpClient to get the network stream for reading and writing. Once you have this you can parse the incoming requests in accordance to whatever protocol (or subset thereof) you want to support and then write your response back with whatever content you want served up to the caller. In this case, I’m looking for http traffic and based upon the uri in the request, serving up just the few things needed to get the silvelight app, stored in the iphone app, to the client browser.

The first thing you need to do is get an address for the TcpListener to listen on. Note this is a bit different between the simulator and the device as the later does not include the “.local” suffix when resolving the host name, so you’ll need to add it.

#if (SIM)
IPAddress localAddr = IPAddress.Parse ("127.0.0.1");
int port = 0;
#else
string hostName = System.Net.Dns.GetHostName () + ".local";
int port = 0;
IPHostEntry hostEntry = Dns.GetHostByName (hostName);
IPAddress localAddr = hostEntry.AddressList[0];
#endif

Once you have the address you can use it to create a TcpListener and as requests come in, parse them to serve up the necessary responses for Silverlight, which are the hosting html page, the Silverlight.js file and the .xap file itself. For a Silverlight app that makes no additional network calls internally, once it’s at the client browser, no additional server support will be needed. If you want additonal calls supported for images, other files or whatever else you can think of, you can implement them in the server in a similar fashion to that used in serving up the xap.

...

while (true) {

RaiseLogEntryEntered ("mtServer is alive and listening");

TcpClient tcpClient = tcpListener.AcceptTcpClient ();

using (NetworkStream netStream = tcpClient.GetStream ()) {

string request = ReadRequest (netStream);

string uri = ParseGetUri (request);

if(uri.EndsWith(".js"))
{
SendSilverlightJavascriptResponse(netStream);
}
else if(uri.EndsWith(".xap"))
{
SendSilverlightXAPResponse(netStream);
}
else //default everything else to the hosting html page (could get more granular here if we want)
{
SendSilverlightHostPageResponse(netStream);
}

tcpClient.Close ();
}

...

The SendXYZ methods are just wrappers on a single call to send a response to the client browser:

void SendResponse(NetworkStream netStream, string contentType, string resourcePath)
{
byte[] resource = File.ReadAllBytes(resourcePath);

string responseHeaders = String.Format ("HTTP/1.1 200 OK\r\nContent-Length: {0}\r\nContent-Type: {1}\r\n\r\n",
resource.Length, contentType);

byte[] responseHeadersEncoded = Encoding.UTF8.GetBytes (responseHeaders);

netStream.Write (responseHeadersEncoded, 0, responseHeadersEncoded.Length);

RaiseLogEntryEntered (String.Format ("Response:\r\n{0}", responseHeaders));

netStream.Write(resource, 0, resource.Length);
}

In the iPhone app I created a view with a UILabel to show the address that the app is listening on and a UITextView to display the raw traffic from the http server.

Here are screenshots of the iPhone app running on the device and the Silverlight app served up to Safari on my Mac: