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:

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

  • Place a UIView on a parent view in the storyboard
  • Set its custom class to MyCustomView
  • 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?


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

    Note that this QA (like many) is really just of historic interest.

    Nowadays For years and years now in iOS everything's just a container view. Full tutorial here

    (Indeed Apple finally added Storyboard References, some time ago now, making it far easier.)

    Here's a typical storyboard with container views everywhere. Everything's a container view. It's just how you make apps.

    在这里输入图像描述

    (As a curiosity, KenC's answer shows exactly how, it used to be done to load an xib to a kind of wrapper view, since you can't really "assign to self".)


    I'm adding this as a separate post to update the situation with the release of Swift. The approach described by LeoNatan works perfectly in Objective-C. However, the stricter compile time checks prevent self being assigned to when loading from the xib file in Swift.

    As a result, there is no option but to add the view loaded from the xib file as a subview of the custom UIView subclass, rather than replacing self entirely. This is analogous to the second approach outlined in the original question. A rough outline of a class in Swift using this approach is as follows:

    @IBDesignable // <- to optionally enable live rendering in IB
    class ExampleView: UIView {
    
        required init(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            initializeSubviews()
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            initializeSubviews()
        }
    
        func initializeSubviews() {
            // below doesn't work as returned class name is normally in project module scope
            /*let viewName = NSStringFromClass(self.classForCoder)*/
            let viewName = "ExampleView"
            let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
                                   owner: self, options: nil)[0] as! UIView
            self.addSubview(view)
            view.frame = self.bounds
        }
    
    }
    

    The downside of this approach is the introduction of an additional redundant layer in the view hierarchy which does not exist when using the approach outlined by LeoNatan in Objective-C. However, this could be taken as a necessary evil and a product of the fundamental way things are designed in Xcode (it still seems crazy to me that it is so difficult to link a custom UIView class with a UI layout in a way that works consistently over both storyboards and from code) – replacing self wholesale in the initializer before never seemed like a particularly interpretable way of doing things, although having essentially two view classes per view doesn't seem so great either.

    Nonetheless, one happy result of this approach is that we no longer need to set the view's custom class to our class file in interface builder to ensure correct behaviour when assigning to self , and so the recursive call to init(coder aDecoder: NSCoder) when issuing loadNibNamed() is broken (by not setting the custom class in the xib file, the init(coder aDecoder: NSCoder) of plain vanilla UIView rather than our custom version will be called instead).

    Even though we cannot make class customizations to the view stored in the xib directly, we are still able to link the view to our 'parent' UIView subclass using outlets/actions etc. after setting the file owner of the view to our custom class:

    设置自定义视图的文件所有者属性

    A video demonstrating the implementation of such a view class step by step using this approach can be found in the following video.

    链接地址: http://www.djcxy.com/p/87702.html

    上一篇: UIPageViewController和故事板

    下一篇: 用xib创建一个可重用的UIView(并从storyboard中加载)