Posts Tagged UIKit

Building iPhone applications using MonoTouch, part 4: the UISearchDisplayController

In my previous post I wrote about the Interface Builder and things like outlets. Last night (with the help of some colleagues) I cracked one of the more advanced Classes in the Interface Builder: the UISearchDisplayController.

In the previous version of my application I had a UITableView and a UISearchBar. I hooked them up with some code and it worked fine. But I didn’t get the effect that you see in (for instance) the iPod application. When you scroll up,  uncover the search-bar and start typing, the original view is greyed-out. And when your search gives no result, you get a “No Results” message. Like this:

SearchScreenShot NoResults

For that, you need the UISearchDisplayController. This controller does the work of hooking up a couple of UI-elements for you:

  1. The search bar
  2. The view with the results from the search (called the searchContentsController)
  3. The delegate that handles all the events that come from the search bar and the results view (called the searchResultsDelegate)
  4. The data source that provides the data to search in (called the searchResultsDataSource)
  5. Your original view

When you drag a UISearchDisplayController and drop it at the top of your UITableViewController, all the outlets get connected to your controller automatically. Apparently the Interface Builder thinks that your class can play all these roles. This makes sense when you program Objective-C, since that language is quite capable of inheriting from lots of base classes (as is so eloquently explained in the Cocao With Love blog), but asks some more attention when used from MonoTouch.

Designers should not write code, and vice versa

I will explain again what Interface Builder does for you. Maybe you already know, but I had to get used to it, and have to keep reminding myself that you use it to, well,  build interfaces. Nothing else. Interface Builder provides a clean separation between the GUI and the code, and that’s a good thing, right? Right. I love Design Patterns, and I try to convince as much progammers as possible to take at least notice of them. But iPhone apps are mostly very small apps with one or two screens, being very good in just one or two things. Do I need to implement this whole pattern for my simple app? Well, apparently. And so will you, so let me try to help you find out how to do some of these things.

Steps to follow

Begin with a UITableViewController. Then go to the Library Window, the Objects button and drag-n-drop a UISearchDisplayController just at the top of your UITableViewController.
In the MainWindow you will have the UISearchDisplayController added. Click it there, and then go to the Outlets-tab in the Inspector Window. You will see that all the outlets will be connected to your UITableViewController, except for the searchBar-outlet since that is of course connected to the SearchBar.

When you run your app, you will have the Table-View and the SearchBar above it. When you tap the text-field you will see the desired gray-out and the “No Result” if you enter some text. So far so good.

Providing the view with data

Now we need to hook up some of our own code to the events that the UISearchDisplayController fires. Let’s start with some data in our own TableView to filter on.
As said in one of my first posts, data for a view is delivered by a DataSource. In this case a UITableViewDataSource. So add a class to your project that inherits from UITableViewDataSource. Something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class LeesPlankjeDataSource : UITableViewDataSource
{
	private string[] woordjes = new string[] {"aap", "noot", "mies"};
 
	public LeesPlankjeDataSource()
	{
	}
 
	public LeesPlankjeDataSource(string filter)
	{
		woordjes = woordjes.Where(f => f.StartsWith(filter)).ToArray();
	}
 
	public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
	{
		UITableViewCell cell = tableView.DequeueReusableCell("plankje");
		if (cell == null)
		{
			cell = new UITableViewCell(UITableViewCellStyle.Default, "plankje");
		}
		cell.TextLabel.Text = woordjes[indexPath.Row];
		return cell;
	}
 
	public override int RowsInSection (UITableView tableview, int section)
	{
		return woordjes.Length;
	}
}

The complete code of this solution is at the bottom of this post.

The class has two constructors. The default constructor (at line 5) is called when initializing our own view, the constructor that takes a string (at line 9) will be called when filtering is started.
The GetCell() and RowsInSection() methods need to be implemented to make your data source work. The implementation is pretty straightforward. The GetCell() method will be called “RowsInSection” times. The call to DequeueReusableCell() is some trick to limit the amount of resources that your iPhone application will use. Just make sure you pass in some string that you reuse a few lines down.

To be able to set this datasource on the table-view we have to have some programmatic access to the view. Well, we did that before in the previous post. Go to Interface Builder, select the AppDelegate in the Library Window, add an outlet and connect it to your UITableViewController. Then you can have code like this in your main.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public partial class AppDelegate : UIApplicationDelegate
{
	// This method is invoked when the application has loaded its UI and its ready to run
	public override bool FinishedLaunching (UIApplication app, NSDictionary options)
	{
		tableView.DataSource = new LeesPlankjeDataSource();
 
		// If you have defined a view, add it here:
		window.AddSubview (tableView);
		window.MakeKeyAndVisible ();
 
		return true;
	}
 
	// This method is required in iPhoneOS 3.0
	public override void OnActivated (UIApplication application)
	{
	}
}

We simply set the DataSource on the tableView to our own DataSource and then add the tableView to the current window. Run your app and you will have some data in your view!

Building the delegate

Now we need some code to handle the events of the search bar. The most interesting event is “ShouldReloadForSearchString()”.

Add a new class to your project that inherits from UISearchDisplayDelegate. The code should look something like this:

1
2
3
4
5
6
7
8
9
10
11
[MonoTouch.Foundation.Register("LeesPlankjeDelegate")]
public class LeesPlankjeDelegate : UISearchDisplayDelegate
{
	public override bool ShouldReloadForSearchString (UISearchDisplayController controller, string forSearchString)
	{
		Console.WriteLine("In ShouldReloadForSearchString");
		controller.SearchResultsDataSource = new LeesPlankjeDataSource(forSearchString);
		return true;
		//return base.ShouldReloadForSearchtring (controller, forSearchString);
	}
}

The first line is interesting. This is the magic that brings your classes into the Interface Builder. By registering the name of your class it will be added to the XIB (although not visible in the designer file). I’ll show you in a minute what you do with this in Interface Builder.

In the override of the ShouldReloadForSearchtring() I instantiate a new data source using the constructor that accepts a filter string. I set this on the SearchResultsDataSource property of the passed in controller object. As you can see in the code of the LeesPlankjeDataSource it will use a Lambda to filter the fixed array of words.

Hooking up the UISearchDisplayController with your Delegate

The Register-attribute on your delegate class makes it available in Interface Builder. So you go to the Library Window, choose the Objects button, then Controllers folder and then the general NSObject. Something like this: LibraryWindow

Drag it to the MainWindow, select it there, go to the Inspector, choose the Identity tab. Now change the class field to the name of your own class. LeesPlankjeDelegate in my case. Your class will not be listed, but that doesn’t matter. When you hit Enter, you’ll see in the MainWindow that both the class name and the instance name have changed. That is just fine.

Now the next magical thing: you have to connect the default delegate of the UISearchDisplayController to your Delegate class. Here is how: select the UISearchDisplayController in the MainWindow, go to the inspector, select the Outlets tab. The first outlet there is called “delegate” and is connected to your TableView. Now remove that connection by clicking the X. Then connect this delegate to your own Delegate class in the MainWindow.

Save in Interface Builder, go to MonoDevelop, run! Type something in the search and “Lo and behold!” it works!

Ain’t live sweet?

If it doesn’t work, feel free to leave a comment. I’ll see if I can help you.

Download the source code

P.S.

The last step is actually more complex than it should have been. If I make my UISearchDisplayController visible to my AppDelegate by adding an outlet, I can do with just one more line of code in my main.cs:

searchDisplayController.Delegate = new LeesPlankjeDelegate();

That way I go one-way: from Interface Builder to MonoTouch. But I thought it more interesting to go the other way too: from MonoTouch to Interface Builder.

, , , , , ,

21 Comments