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.

, , , , , ,

  1. #1 door MonoTouch.Info om 3 november 2009

    Thank you for submitting this entry to http://monotouch.info!

  2. #2 door Christian Weyer om 5 november 2009

    Cool stuff!
    Do you have any idea how I can add an image into the upper area, together with the search bar… preferably in IB ;)

    Thanks.

  3. #3 door Richard de Zwart om 5 november 2009

    Well, there is a challenge! ;-)
    I read an interesting article on the iPhone Dev Center that explains how you make sub-views and such. I think the answer is in that direction.

  4. #4 door Christian Weyer om 5 november 2009

    AH, OK – using tables… thanks.

  5. #5 door K James om 6 november 2009

    Hi
    The iPhone has revolutionized the cell phone industry. No longer are our cell phones just for makings calls. Online graduates are now demanding more and more capabilities from their iPhones.

  6. #6 door Maurice om 9 november 2009

    Leuk om te zien wat je met MonoTouch aan het doen bent. Heb je zin om daar een keer een presentatie bij de SDN over te geven?

  7. #7 door Mike Rutherford om 7 februari 2010

    Very nice … and thanks for including the P.S. :)

  8. #8 door Richard de Zwart om 14 juni 2010

    Stefan wrote (at another of my blog posts):
    hi,

    why do you get the localized message for “no results”? I would like to have the german version there… but I get “no results” – do you know what i have to do?

    thanks a lot,

    stefan

    ======================

    @Stefan: I have to disappoint you. I have not yet looked into internationalization on the iPhone.
    In the sample image I added to the article, I took a snapshot from the iTunes app. Apparently that application is locale-aware.
    When you go to Project Settings in MonoDevelop, there is an option I18n (which is short for internationalization) that lets you add language-assemblies. I could not get that working yet, but maybe it helps you.

  9. #9 door Richard de Zwart om 14 juni 2010

    I tried the i18n options (as described at http://monotouch.net/Documentation/Internationalization), but that did not give the expected result. I used “west” since that contains the 1252 code page.
    But code pages are about characters anyway, and not about the language itself. So the UISearchDisplayController should have some way to indicatie that it should load the “No results” text from a different bundle.

  10. #10 door Richard de Zwart om 15 juni 2010

    After trying the same sample in XCode, I found out that you have to set the “Localization native development region” to “Dutch” or “German” or whatever pleases you.
    In MonoTouch you must add this property to the Info.plist. Double-click this file to open up the Property List Editor and then click the “Add Child” button to select the right property.

  11. #11 door Stefan om 15 juni 2010

    Hi Richard,

    thanks a loooot for your help – it’s really appreciated!

    Best regards,

    Stefan

  12. #12 door Dennis Lee om 26 september 2010

    Dennis Lee :
    Hi,
    thanks for this article, it was most helpful. I’ve tried to use a modification of this, but am running into problems and was wondering if you could help me.
    In your sample, the searchbar, tableview, search delegate and searchContentsController are all embedded in the main window. I want to move this to a separate NIB file and then add this to the main window (inside a navigation controller).
    When I do this, the search delegate never gets called. I added an outlet that is mapped to the searchContentsController and upon the ViewDidLoad being called, inspected the contents of the outlet and it was null, leading me to believe that it (and the search delegate) are not being created.
    Any ideas what the problem is?
    thanks, your help would be most appreciated,
    Dennis

  13. #13 door Richard de Zwart om 30 september 2010

    @Dennis Lee:
    I recently moved an iPhone-only application to a Universal App and created just what you want. So I have a NavigationController with the searchbar/tableview/searchdelegate in it, loaded from a NIB. It works wonderful, also on the iPad.
    I looked at that code and saw no special tricks.
    The controller-with-nib that presents the list to be searched inherits from UITableViewController. In the Initialize() function that gets generated for you by MonoDevelop I set the searchDisplayController.Delegate to my own delegate-class. I have an outlet in the “File’s Owner” that is linked to the actual SearchDisplayController. My delegate class only overrides the “ShouldReloadForSearchString”.

    Maybe if you send me some code? I could look into it.

  14. #14 door Dennis om 5 oktober 2010

    Hi Richard,
    thanks very much for the reply. I ended up creating the searchDisplayController and the delegate manually in code and linking them up. For some reason, when I tried doing it in IB, they were always null.

    I’m not sure what I did wrong, but I must have kept on doing the same thing (or not doing something). Anyways, I now have a working solution.

    Thanks again for this posting, as it is very helpful.

    Dennis

  15. #15 door kumud om 15 februari 2011

    Thanks
    nice examples. Could please provide me sample code for opening another view on click/tapping of selected row.

    thanks

  16. #16 door Richard de Zwart om 23 februari 2011

  17. #17 door Sarah om 8 juni 2011

    Thank you so much for this information! It is so well written out and easy to understand. Just what I need! :)

  18. #18 door Gordo om 8 juni 2011

    I am fascinated at the talent I see displayed on this website. Please continue to provide such profound information

  19. #19 door Joe Hoper om 9 oktober 2011

    Great article, exactly what I needed to get started — thank you very much!

  20. #20 door miliu om 26 november 2011

    Hi, I’m trying to use the UISearchBar and found your interesting article. However, I got some problems to follow you (not sure if it is because I’m using the new MonoDevelop).

    1. How do I get UITableViewController in MonoTouch? I couldn’t find this option. I only have the option to create UIViewController. Did I miss some configurations?

    2. Where do I find UISearchDisplayController? In my IB, I could see one view called “Search Bar and Search Display Controller” with subtitle “UISearchBar”. However, after I drag-n-drop it, I got UISearchBar. What’s wrong?

  21. #21 door Morten Slott Hansen om 23 oktober 2012

    This does not work. The initial error message is “loaded the “TableSearchViewController” nib but the view outlet was not set.”
    Setting this from “Files Owner” changes the error message to
    “A view can only be associated with at most one view controller at a time!”

    This is on the latest xcode editor 4.5.1.

    Any help would be much appreciated.

(wordt niet gepubliceerd)