Finally, I have managed to work on one of my favorite iOS subjects: NIB files. It’s in the list of my favorites because of their complexity, at least from a C# developer’s point of view. Whenever I can, I create my UI in NIBs. Let’s face it. It is always better to write less code, so why not? Read below for some of my (hopefully) useful findings.

How a controller loads its UI

Let’s start with some basic stuff. When we add a new “iPhone View Controller” in a MonoTouch project, MonoDevelop declares a class for us like this (I named it “MainController”):

It also adds some more useful stuff, but we don’t care about them here. What we care about is the constructor above. The default constructor of our class calls the UIViewController(string nibName, NSBundle mainBundle) constructor. This constructor is exposed to the Objective-C runtime as the “initWithNibName:bundle:” instance method, through the MonoTouch ExportAttribute. What this method basically does is to search inside our app’s main bundle for the NIB file with the name we pass to it. To allow the runtime to find the file, we must not include any paths or a file extension. Also, when passing null to the NSBundle parameter, we tell the runtime to search in the main bundle of our app. So, simply put: we create an instance and the interface is loaded from the file system automatically, when we do the following:

MainController mController = new MainController();

… almost. The NIB file is actually loaded when the view of our controller is first accessed. This can be checked by entering some code in the ViewDidLoad method. You will see that the initialization of the class does not trigger the ViewDidLoad method. But if, for example, we do the following:

mController.View.Hidden = false;

the ViewDidLoad method will be called. That’s the basic stuff. Let’s move on to a bit more complex situations.

Complex situations

What if I want to create new instances of MainController multiple times in the life time of my app? That will mean that every time the class is instantiated, the same NIB file will have to be loaded also. But why should that happen? Shouldn’t I be able to create multiple instances of the same class, without having to load its interface from the file system every single time? Well, we can do that. Meet the UINib class. This class reads a NIB file from the file system once and instantiates its user interface when we want it to. It does all we want with only two methods. Here’s how it works:

The above line loads a NIB into memory. This NIB is now ready for us to use. It accepts the same parameters as the UIViewController constructor we saw earlier. One small difference is that we cannot pass null to it in MonoTouch, although its default implementation in Objective-C accepts nil. No harm done. Anyway, now that we have loaded our NIB into memory, let’s get some UI from it for our controller. But, we have to change something first. Since the code created by MonoDevelop automatically adds the call to the base constructor, we have to remove that. We do not want the initWithNibName:bundle: method to be called. Let’s comment it out:

Ready! Our controller is created and it has a UI!. There is an automation we loose with this though. The ViewDidLoad method will not get called. So, let’s just call it ourselves:

mController.ViewDidLoad();

All other methods that should get called automatically, are called (eg. ViewWillAppear, ViewDidAppear, ViewDidUnload etc.).
And, we are done with the controller UI. In theory, it should be more efficient than loading the same UI from the file system each time we want to present it. Of course, we should not forget a basic rule: when NIBs are loaded, all of their contents are loaded into memory, so the more UI objects a NIB file contains, the slower it will get. So performance always comes along with good design practices.

More info on the InstantiateWithOwneroptions method and some more NIB stuff

Let’s take a look at the method signature. No need for its native version, the C# version will do just fine:

As you can see, the method not only instantiates a NIB for us, but it returns some objects also. We’ll talk about these in a moment, let’s talk about the parameters first. The “ownerOrNil” parameter is the owner of the NIB contents. Basically, it is the instance that the NIB’s “File’s Owner” will be represented by. In our case above, we pass the “mController” object to it. The File’s Owner in a NIB file, is the interface that exposes the contents of the file to us. Along with the First Responder we see in a NIB, these are Placeholder objects. We can forget about the parameter name’s “-OrNil” suffix. We must not pass null to it because we’ll get an exception. That is like saying “I want to get into a room without opening the door”.

The second parameter, “optionsOrNil” is for instructing the runtime how to handle external objects (or, Proxy objects). For example, if we have a view controller in a NIB file, but the UI for that view controller is loaded from another NIB file, we must pass this information or that NIB file will not be loaded. Consider the following scenario: in a NIB file, we have a UINavigationController with its root view controller connected to a controller form another NIB file (this root view controller in the first NIB file is the Proxy object). It is the “optionsOrNill” parameter we will use to load everything correctly:

Simply put, we need one NSDictionary to hold the owner(s) of the Proxy object(s) (we can include multiple Proxy objects) and one NSDictionary that will contain the first NSDictionary, with the key “UINibExternalObjects”.

How about the return value? This is where it gets a bit more interesting. The return value contains the Top-Level objects of the NIB. Top-Level objects are all the objects in a NIB that do not have a parent object and are not Placeholder objects. So, if we have a NIB that contains a UIViewController as File’s Owner (Placeholder object), the method will return one item in the NSObject[]: the view controller’s View. Anything on the same hierarchy level of that View, is a Top-Level object. If that View has subviews, these views are not Top-Level objects. I believe the following screenshot makes it clear:

Enough with the theory

Ok, so why do we need all this stuff? My opinion is that the most important role of the above information is that it will allow us to use NIB files more efficiently. Efficiency in this case, comes from the ability to cache the NIBs that will be used multiple times in the life time of our app. For this reason, I have created JTNibManager, a small MonoTouch library that will make all this stuff easier. You can get it from GitHub here (this is my first repo over there, so please excuse me for any strange stuff/files you may encounter), along with a sample project that shows how to use it. Let’s get to the bottom of it:

class NibManager : NSObject

This is the class that does all the NIB caching/loading for us. All that we need is enclosed in two methods. It stores all NIBs in a Dictionary<string, UINib> and provides us with the ability to instantiate them whenever we want. These are the methods:

This is just a helper abstract class. We can derive from it and the NIB associated with a controller will be automatically cached.

Using JTNibManager

The following examples are from the included NibManagerTest project. The first thing we need is to create a Singleton for the NibManager, so it will be accessible throughout all the objects. I usually do this in my AppDelegate:

This is where the NIB is loaded and cached. To help keep our code clean, I have included the call to ViewDidLoad in there.

The above shows how to use the library for controllers. How about single UI objects? Like views or table cells? No worries, that can also be done. For this blog post, I am skipping on the “how to create a custom view/custom cell in a NIB” stuff. Inside the Views directory of the NibManagerTest project you will find a custom view with its XIB. Here’s how that view is loaded:

// Loads a view from a XIB
// “this” is just a view controller that the custom view will be used in.
CustomView customView = AppDelegate.NibManager.LoadUIObject<CustomView>(this, “CustomView”, NSBundle.MainBundle);

The above solution is way better than the one I wrote in my previous article “Loading custom UITableViewCells from Nib files without a controller in MonoTouch” and is based on Alex York’s solution. The difference here is that this uses the JTNibManager library, so the NIB files of the cells are cached into memory and we do not have to load them each time. So if, for example, you have a UITableView whose rows are presented through multiple instances of only one custom cell, there is no need to load the NIB file each time.

How about Outlets and Actions?

Didn’t forget about them. The JTNibManager library will work as it should, however some considerations are worth mentioning. Here’s a list of them:

Make sure that the object you pass as an owner to NibManager’s methods, already contain the same outlets that exist in the NIB. Outlets in MonoTouch are exposed through the [Outlet] attribute and can either be found in the auto-generated files “SomeController.designer.cs”, or we can create them ourselves:

// When the XIB file is double-clicked in MonoDevelop,
// it will automatically create an outlet named “myOutlet”
// for us in the corresponding .h file.
// We will still have to connect it to the UI object ourselves (Ctrl-drag).
[Outlet(“myOutlet”)]
public UILabel SomeLabel { get; set; }

The same applies for actions, which are marked in a class with the [Action] attribute.

When creating outlets in the classes with the [Outlet] attribute, make sure to provide full access to them so that the runtime can set it when it loads the UI. The following will raise an “NSInternalInconsistency” exception upon creation:

When creating standalone UI objects in NIBs (like a custom UITableViewCell), there is no need to connect the cell to the File’s Owner through an outlet. This is because the UINib.InstantiateWithOwneroptions returns the Top-Level objects, so a standalone cell in a NIB is a Top-Level object. Hence, the NibManager.LoadUIObject<TUIObject> method returns the first Top-Level object it gets from the InstantiateWithOwneroptions call. If you load a custom table cell from a NIB and you get an exception regarding “setValueForUndefinedKey”, it most likely means that the runtime did not find an outlet where one was expected.

The “standard” rules apply regarding cleaning: release any outlets in the ViewDidUnload override.

Note that the above considerations are the same for using NIB files normally, without JTNibManager or similar.

Closing

I hope you will find the above useful. There’s more to NIB loading/caching than what is mentioned here. In fact, there is something even more useful than just caching the NIBs. Let me give you a hint: selectable skins…