Ken Chatfield Ken Chatfield - 6 months ago 94
iOS Question

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

OK, there are dozens of posts on stack overflow about this, but none are particularly clear on the solution. I'd like to create a custom UIView with accompanying XIB. 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 programatically 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
[init super]
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 it's 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];
    }
    }

    -(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?

Answer

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;
}