Search code examples
iosobjective-cuiviewstoryboardnib

Creating a reusable UIView with xib (and loading from storyboard)


OK, there are dozens of posts on StackOverflow about this, but none are particularly clear on the solution. I'd like to create a custom UIView with an accompanying xib file. The requirements are:

  • No separate UIViewController – a completely self-contained class
  • Outlets in the class to allow me to set/get properties of the view

My current approach to doing this is:

  1. Override -(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    
  2. Instantiate programmatically using -(id)initWithFrame: in my view controller

    MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    [self.view insertSubview:myCustomView atIndex:0];
    

This works fine (although never calling [super init] and simply setting the object using the contents of the loaded nib seems a bit suspect – there is advice here to add a subview in this case which also works fine). However, I'd like to be able to instantiate the view from the storyboard also. So I can:

  1. Place a UIView on a parent view in the storyboard
  2. Set its custom class to MyCustomView
  3. Override -(id)initWithCoder: – the code I've seen the most often fits a pattern such as the following:

    -(id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(void)initializeSubviews {
        typeof(view) view = [[[NSBundle mainBundle]
                             loadNibNamed:NSStringFromClass([self class])
                                    owner:self
                                  options:nil] objectAtIndex:0];
        [self addSubview:view];
    }
    

Of course, this doesn't work, as whether I use the approach above, or whether I instantiate programatically, both end up recursively calling -(id)initWithCoder: upon entering -(void)initializeSubviews and loading the nib from file.

Several other SO questions deal with this such as here, here, here and here. However, none of the answers given satisfactorily fixes the problem:

  • A common suggestion seems to be to embed the entire class in a UIViewController, and do the nib loading there, but this seems suboptimal to me as it requires adding another file just as a wrapper

Could anyone give advice on how to resolve this problem, and get working outlets in a custom UIView with minimum fuss/no thin controller wrapper? Or is there an alternative, cleaner way of doing things with minimum boilerplate code?


Solution

  • Your problem is calling loadNibNamed: from (a descendant of) initWithCoder:. loadNibNamed: internally calls initWithCoder:. If you want to override the storyboard coder, and always load your xib implementation, I suggest the following technique. Add a property to your view class, and in the xib file, set it to a predetermined value (in User Defined Runtime Attributes). Now, after calling [super initWithCoder:aDecoder]; check the value of the property. If it is the predetermined value, do not call [self initializeSubviews];.

    So, something like this:

    -(instancetype)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
    
        if (self && self._xibProperty != 666)
        {
            //We are in the storyboard code path. Initialize from the xib.
            self = [self initializeSubviews];
    
            //Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
            //self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
        }
    
        return self;
    }
    
    -(instancetype)initializeSubviews {
        id view =   [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
    
        return view;
    }