Load view from an external xib file in storyboard

后端 未结 10 2219
一生所求
一生所求 2020-11-27 09:49

I want to use a view throughout multiple viewcontrollers in a storyboard. Thus, I thought about designing the view in an external xib so changes are reflected in every viewc

相关标签:
10条回答
  • 2020-11-27 10:19

    Best solution currently is to just use a custom view controller with its view defined in a xib, and simply delete the "view" property that Xcode creates inside the storyboard when adding the view controller to it (don't forget to set the name of the custom class though).

    This will make the runtime automatically look for the xib and load it. You can use this trick for any kind of container views, or content view.

    0 讨论(0)
  • 2020-11-27 10:21

    Although the top most popular answers works fine, they are conceptually wrong. They all use File's owner as connection between class's outlets and UI components. File's owner is supposed to be used only for top-level objects not UIViews. Check out Apple developer document. Having UIView as File's owner leads to these undesirable consequences.

    1. You are forced to use contentView where you are supposed to use self. It’s not only ugly, but also structurally wrong because the intermediate view keeps data structure from conveying it’s UI structure. It's like going against the concept of declarative UI.
    2. You can only have one UIView per Xib. An Xib is supposed to have multiple UIViews.

    There's elegant way to do it without using File's owner. Please check this blog post. It explains how to do it the right way.

    0 讨论(0)
  • 2020-11-27 10:22

    Here's the answer you've wanted all along. You can just create your CustomView class, have the master instance of it in a xib with all the subviews and outlets. Then you can apply that class to any instances in your storyboards or other xibs.

    No need to fiddle with File's Owner, or connect outlets to a proxy or modify the xib in a peculiar way, or add an instance of your custom view as a subview of itself.

    Just do this:

    1. Import BFWControls framework
    2. Change your superclass from UIView to NibView (or from UITableViewCell to NibTableViewCell)

    That's it!

    It even works with IBDesignable to refer your custom view (including the subviews from the xib) at design time in the storyboard.

    You can read more about it here: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155

    And you can get the open source BFWControls framework here: https://github.com/BareFeetWare/BFWControls

    And here's a simple extract of the NibReplaceable code that drives it, in case you're curious:
https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

    Tom

    0 讨论(0)
  • 2020-11-27 10:24

    This solution can be used even if your class does not have the same name as the XIB. For example, if you have a base view controller class controllerA which has a XIB name controllerA.xib and you subclassed this with controllerB and want to create an instance of controllerB in a storyboard, then you can:

    • create the view controller in the storyboard
    • set the class of the controller to the controllerB
    • delete the view of the controllerB in the storyboard
    • override load view in controllerA to:

    *

    - (void) loadView    
    {
            //according to the documentation, if a nibName was passed in initWithNibName or
            //this controller was created from a storyboard (and the controller has a view), then nibname will be set
            //else it will be nil
            if (self.nibName)
            {
                //a nib was specified, respect that
                [super loadView];
            }
            else
            {
                //if no nib name, first try a nib which would have the same name as the class
                //if that fails, force to load from the base class nib
                //this is convenient for including a subclass of this controller
                //in a storyboard
                NSString *className = NSStringFromClass([self class]);
                NSString *pathToNIB = [[NSBundle bundleForClass:[self class]] pathForResource: className ofType:@"nib"];
                UINib *nib ;
                if (pathToNIB)
                {
                    nib = [UINib nibWithNibName: className bundle: [NSBundle bundleForClass:[self class]]];
                }
                else
                {
                    //force to load from nib so that all subclass will have the correct xib
                    //this is convenient for including a subclass
                    //in a storyboard
                    nib = [UINib nibWithNibName: @"baseControllerXIB" bundle:[NSBundle bundleForClass:[self class]]];
                }
    
                self.view = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
           }
    }
    
    0 讨论(0)
  • 2020-11-27 10:32

    Solution for Objective-C according to steps described in Ben Patch's response.

    Use extension for UIView:

    @implementation UIView (NibLoadable)
    
    - (UIView*)loadFromNib
    {
        UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
        xibView.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:xibView];
        [xibView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
        [xibView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
        [xibView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
        [xibView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
        return xibView;
    }
    
    @end
    

    Create files MyView.h, MyView.m and MyView.xib.

    First prepare your MyView.xib as Ben Patch's response says so set class MyView for File's owner instead of main view inside this XIB.

    MyView.h:

    #import <UIKit/UIKit.h>
    
    IB_DESIGNABLE @interface MyView : UIView
    
    @property (nonatomic, weak) IBOutlet UIView* someSubview;
    
    @end
    

    MyView.m:

    #import "MyView.h"
    #import "UIView+NibLoadable.h"
    
    @implementation MyView
    
    #pragma mark - Initializers
    
    - (id)init
    {
        self = [super init];
        if (self) {
            [self loadFromNib];
            [self internalInit];
        }
        return self;
    }
    
    - (id)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self) {
            [self loadFromNib];
            [self internalInit];
        }
        return self;
    }
    
    - (id)initWithCoder:(NSCoder *)aDecoder
    {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self loadFromNib];
        }
        return self;
    }
    
    - (void)awakeFromNib
    {
        [super awakeFromNib];
        [self internalInit];
    }
    
    - (void)internalInit
    {
        // Custom initialization.
    }
    
    @end
    

    And later just create your view programatically:

    MyView* view = [[MyView alloc] init];
    

    Warning! Preview of this view will not be shown in Storyboard if you use WatchKit Extension because of this bug in Xcode >= 9.2: https://forums.developer.apple.com/thread/95616

    0 讨论(0)
  • 2020-11-27 10:36

    My full example is here, but I will provide a summary below.

    Layout

    Add a .swift and .xib file each with the same name to your project. The .xib file contains your custom view layout (using auto layout constraints preferably).

    Make the swift file the xib file's owner.

    Code

    Add the following code to the .swift file and hook up the outlets and actions from the .xib file.

    import UIKit
    class ResuableCustomView: UIView {
    
        let nibName = "ReusableCustomView"
        var contentView: UIView?
    
        @IBOutlet weak var label: UILabel!
        @IBAction func buttonTap(_ sender: UIButton) {
            label.text = "Hi"
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            guard let view = loadViewFromNib() else { return }
            view.frame = self.bounds
            self.addSubview(view)
            contentView = view
        }
    
        func loadViewFromNib() -> UIView? {
            let bundle = Bundle(for: type(of: self))
            let nib = UINib(nibName: nibName, bundle: bundle)
            return nib.instantiate(withOwner: self, options: nil).first as? UIView
        }
    }
    

    Use it

    Use your custom view anywhere in your storyboard. Just add a UIView and set the class name to your custom class name.

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