Teaser Image

qnoid

Markos Charatzas - London, UK




There seems to be a lot of confusion around custom views on iOS that can be used both at compile time under Interface Builder and at runtime.

Even though a lot of posts exist that show how to design a UIView with a custom hierarchy using Interface Builder, there seems to be unnecessary complexity on what comes next.

1. Design the look and feel and view hierarchy in Interface Builder

The first thing to do is create a xib file (TBView.xib) as to design in Interface Builder the look and feel of your custom view along with its hierarchy.

By convention, this nib should hold a single top-level view.

The code to load any views that are in a xib file is the following.

    NSBundle *mainBundle = [NSBundle mainBundle];
    NSArray *views = [mainBundle loadNibNamed:@"TBView" 
                                        owner:nil 
                                      options:nil];

Where views is a collection of every top-level view in the xib file. (in this case a single one)

Will come back later to see what role the owner plays in loading the view.

2. Create a subclass of UIView as your custom view

In order to use the above look and feel in your custom view and be able to do so under Interface Builder^1 you need to create a subclass of UIView.

    #import <UIKit/UIKit.h>

    @interface TBView : UIView

    @end

By convention, the name of your subclass should be the same as the xib file.

3. Using it at compile time under Interface Builder

In your UIViewController's xib, drag a view and set the "Class" as "TBView" in the "Custom Class" section under "Identity Inspector". (CMD+OPTION+3)

By convention, this view should have the same size as the look and feel design.

Any view in a nib file that is part of the view hierarchy is initialised using NSCoding#initWithCoder: hence this is the spot where you load the #view from the xib and add it as a subclass.

    -(id)initWithCoder:(NSCoder *)aDecoder
    {
        self = [super initWithCoder:aDecoder];
        if(!self){
            return nil;
        }

        NSBundle *mainBundle = [NSBundle mainBundle];
        NSArray *views = [mainBundle loadNibNamed:NSStringFromClass([self class]) 
                                            owner:nil 
                                          options:nil];
        [self addSubview:views[0]];
        
    return self;
    }

Notice how we are using the name of the class, which is the same as the xib by convention to load the view.

This is how the hierarchy looks like.

$0 = 0x0756c810 <TBView: 0x756bf50; frame = (0 0; 320 274); autoresize = W+H; layer = <CALayer: 0x756bfd0>>
       | <UIView: 0x756c050; frame = (0 0; 320 274); autoresize = RM+BM; layer = <CALayer: 0x756c0b0>>
       |    | <UILabel: 0x756ca70; frame = (0 126; 320 21); text = 'Hello World'; clipsToBounds = YES; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x756c0e0>>

4. Using it at runtime given a frame

Simply override UIView#initWithFrame: and use the same code to load the view from the xib and add it in the hierarchy.

    - (id)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if(!self){
            return nil;
        }
        
        NSBundle *mainBundle = [NSBundle mainBundle];
        NSArray *views = [mainBundle loadNibNamed:NSStringFromClass([self class]) 
                                            owner:nil 
                                          options:nil];
        [self addSubview:views[0]];

    return self;
    }

You can now create a new instance of that view as usual.

TBView *view = [[TBView alloc] initWithFrame:frame];

5. File's Owner and IBOutlets

If you need to keep references to any of the views in the xib, you can use the custom view as the File's Owner and define any IBOutlets as properties of that class.

        NSBundle *mainBundle = [NSBundle mainBundle];
        NSArray *views = [mainBundle loadNibNamed:NSStringFromClass([self class]) 
                                            owner:self 
                                          options:nil];
        [self addSubview:views[0]];

As part of loading the views from the xib, the view will have its IBOutlets set.

By convention, the custom class should be the File's Owner thus keeping all related code in a single class that can be reused.

Here is a macro that you can use in both initalisers to load a single view hierarchy that is available in the main bundle.

    #define loadView() \
    NSBundle *mainBundle = [NSBundle mainBundle]; \
    NSArray *views = [mainBundle loadNibNamed:NSStringFromClass([self class]) 
                                        owner:self 
                                      options:nil]; \
    [self addSubview:views[0]];

Source Code

References
NSNibAwaking Protocol Reference
Nib Files

^1: You can equally load the top-level view as designed in Interface Builder and use it at compile time.