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

前端 未结 6 1026
孤独总比滥情好
孤独总比滥情好 2020-11-28 18:49

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 accompan

相关标签:
6条回答
  • 2020-11-28 19:16

    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:

    Setting the file owner property of the custom view

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

    0 讨论(0)
  • 2020-11-28 19:25

    Don't forget

    Two important points:

    1. Set the File's Owner of the .xib to class name of your custom view.
    2. Don't set the custom class name in IB for the .xib's root view.

    I came to this Q&A page several times while learning to make reusable views. Forgetting the above points made me waste a lot of time trying to find out what was causing infinite recursion to happen. These points are mentioned in other answers here and elsewhere, but I just want to reemphasize them here.

    My full Swift answer with steps is here.

    0 讨论(0)
  • 2020-11-28 19:27

    There is a solution which is much more cleaner than the solutions above: https://www.youtube.com/watch?v=xP7YvdlnHfA

    No Runtime properties, no recursive call problem at all. I tried it and it worked like a charm using from storyboard and from XIB with IBOutlet properties (iOS8.1, XCode6).

    Good luck for coding!

    0 讨论(0)
  • 2020-11-28 19:28

    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;
    }
    
    0 讨论(0)
  • 2020-11-28 19:28

    STEP1. Replacing self from Storyboard

    Replacing self in initWithCoder: method will fail with following error.

    'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
    

    Instead, you can replace decoded object with awakeAfterUsingCoder: (not awakeFromNib). like:

    @implementation MyCustomView
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
    @end
    

    STEP2. Preventing recursive call

    Of course, this also causes recursive call problem. (storyboard decoding -> awakeAfterUsingCoder: -> loadNibNamed: -> awakeAfterUsingCoder: -> loadNibNamed: -> ...)
    So you have to check current awakeAfterUsingCoder: is called in Storyboard decoding process or XIB decoding process. You have several ways to do that:

    a) Use private @property which is set in NIB only.

    @interface MyCustomView : UIView
    @property (assign, nonatomic) BOOL xib
    @end
    

    and set "User Defined Runtime Attributes" only in 'MyCustomView.xib'.

    Pros:

    • None

    Cons:

    • Simply does not work: setXib: will be called AFTER awakeAfterUsingCoder:

    b) Check if self has any subviews

    Normally, you have subviews in the xib, but not in the storyboard.

    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        if(self.subviews.count > 0) {
            // loading xib
            return self;
        }
        else {
            // loading storyboard
            return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                  owner:nil
                                                options:nil] objectAtIndex:0];
        }
    }
    

    Pros:

    • No trick in Interface Builder.

    Cons:

    • You cannot have subviews in your Storyboard.

    c) Set a static flag during loadNibNamed: call

    static BOOL _loadingXib = NO;
    
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        if(_loadingXib) {
            // xib
            return self;
        }
        else {
            // storyboard
            _loadingXib = YES;
            typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                               owner:nil
                                                             options:nil] objectAtIndex:0];
            _loadingXib = NO;
            return view;
        }
    }
    

    Pros:

    • Simple
    • No trick in Interface Builder.

    Cons:

    • Not safe: static shared flag is dangerous

    d) Use private subclass in XIB

    For example, declare _NIB_MyCustomView as a subclass of MyCustomView. And, use _NIB_MyCustomView instead of MyCustomView in your XIB only.

    MyCustomView.h:

    @interface MyCustomView : UIView
    @end
    

    MyCustomView.m:

    #import "MyCustomView.h"
    
    @implementation MyCustomView
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        // In Storyboard decoding path.
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
    @end
    
    @interface _NIB_MyCustomView : MyCustomView
    @end
    
    @implementation _NIB_MyCustomView
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        // In XIB decoding path.
        // Block recursive call.
        return self;
    }
    @end
    

    Pros:

    • No explicit if in MyCustomView

    Cons:

    • Prefixing _NIB_ trick in xib Interface Builder
    • relatively more codes

    e) Use subclass as placeholder in Storyboard

    Similar to d) but use subclass in Storyboard, original class in XIB.

    Here, we declare MyCustomViewProto as a subclass of MyCustomView.

    @interface MyCustomViewProto : MyCustomView
    @end
    @implementation MyCustomViewProto
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        // In storyboard decoding
        // Returns MyCustomView loaded from NIB.
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
    @end
    

    Pros:

    • Very safe
    • Clean; No extra code in MyCustomView.
    • No explicit if check same as d)

    Cons:

    • Need to use subclass in storyboard.

    I think e) is the safest and cleanest strategy. So we adopt that here.

    STEP3. Copy properties

    After loadNibNamed: in 'awakeAfterUsingCoder:', You have to copy several properties from self which is decoded instance f the Storyboard. frame and autolayout/autoresize properties are especially important.

    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                           owner:nil
                                                         options:nil] objectAtIndex:0];
        // copy layout properities.
        view.frame = self.frame;
        view.autoresizingMask = self.autoresizingMask;
        view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
    
        // copy autolayout constraints
        NSMutableArray *constraints = [NSMutableArray array];
        for(NSLayoutConstraint *constraint in self.constraints) {
            id firstItem = constraint.firstItem;
            id secondItem = constraint.secondItem;
            if(firstItem == self) firstItem = view;
            if(secondItem == self) secondItem = view;
            [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                                attribute:constraint.firstAttribute
                                                                relatedBy:constraint.relation
                                                                   toItem:secondItem
                                                                attribute:constraint.secondAttribute
                                                               multiplier:constraint.multiplier
                                                                 constant:constraint.constant]];
        }
    
        // move subviews
        for(UIView *subview in self.subviews) {
            [view addSubview:subview];
        }
        [view addConstraints:constraints];
    
        // Copy more properties you like to expose in Storyboard.
    
        return view;
    }
    

    FINAL SOLUTION

    As you can see, this is a bit of boilerplate code. We can implement them as 'category'. Here, I extend commonly used UIView+loadFromNib code.

    #import <UIKit/UIKit.h>
    
    @interface UIView (loadFromNib)
    @end
    
    @implementation UIView (loadFromNib)
    
    + (id)loadFromNib {
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
    
    - (void)copyPropertiesFromPrototype:(UIView *)proto {
        self.frame = proto.frame;
        self.autoresizingMask = proto.autoresizingMask;
        self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
        NSMutableArray *constraints = [NSMutableArray array];
        for(NSLayoutConstraint *constraint in proto.constraints) {
            id firstItem = constraint.firstItem;
            id secondItem = constraint.secondItem;
            if(firstItem == proto) firstItem = self;
            if(secondItem == proto) secondItem = self;
            [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                                attribute:constraint.firstAttribute
                                                                relatedBy:constraint.relation
                                                                   toItem:secondItem
                                                                attribute:constraint.secondAttribute
                                                               multiplier:constraint.multiplier
                                                                 constant:constraint.constant]];
        }
        for(UIView *subview in proto.subviews) {
            [self addSubview:subview];
        }
        [self addConstraints:constraints];
    }
    

    Using this, you can declare MyCustomViewProto like:

    @interface MyCustomViewProto : MyCustomView
    @end
    
    @implementation MyCustomViewProto
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        MyCustomView *view = [MyCustomView loadFromNib];
        [view copyPropertiesFromPrototype:self];
    
        // copy additional properties as you like.
    
        return view;
    }
    @end
    

    XIB:

    XIB screenshot

    Storyboard:

    Storyboard

    Result:

    enter image description here

    0 讨论(0)
  • 2020-11-28 19:30

    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.

    enter image description here

    (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".)

    0 讨论(0)
提交回复
热议问题