Load view from an external xib file in storyboard

后端 未结 10 2220
一生所求
一生所求 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:37

    For a while Christopher Swasey's approach was the best approach I had found. I asked a couple of the senior devs on my team about it and one of them had the perfect solution! It satisfies every one of the concerns that Christopher Swasey so eloquently addressed and it doesn't require boilerplate subclass code(my main concern with his approach). There is one gotcha, but other than that it is fairly intuitive and easy to implement.

    1. Create a custom UIView class in a .swift file to control your xib. i.e. MyCustomClass.swift
    2. Create a .xib file and style it as you want. i.e. MyCustomClass.xib
    3. Set the File's Owner of the .xib file to be your custom class (MyCustomClass)
    4. GOTCHA: leave the class value (under the identity Inspector) for your custom view in the .xib file blank. So your custom view will have no specified class, but it will have a specified File's Owner.
    5. Hook up your outlets as you normally would using the Assistant Editor.
      • NOTE: If you look at the Connections Inspector you will notice that your Referencing Outlets do not reference your custom class (i.e. MyCustomClass), but rather reference File's Owner. Since File's Owner is specified to be your custom class, the outlets will hook up and work propery.
    6. Make sure your custom class has @IBDesignable before the class statement.
    7. Make your custom class conform to the NibLoadable protocol referenced below.
      • NOTE: If your custom class .swift file name is different from your .xib file name, then set the nibName property to be the name of your .xib file.
    8. Implement required init?(coder aDecoder: NSCoder) and override init(frame: CGRect) to call setupFromNib() like the example below.
    9. Add a UIView to your desired storyboard and set the class to be your custom class name (i.e. MyCustomClass).
    10. Watch IBDesignable in action as it draws your .xib in the storyboard with all of it's awe and wonder.

    Here is the protocol you will want to reference:

    public protocol NibLoadable {
        static var nibName: String { get }
    }
    
    public extension NibLoadable where Self: UIView {
    
        public static var nibName: String {
            return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
        }
    
        public static var nib: UINib {
            let bundle = Bundle(for: Self.self)
            return UINib(nibName: Self.nibName, bundle: bundle)
        }
    
        func setupFromNib() {
            guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
            addSubview(view)
            view.translatesAutoresizingMaskIntoConstraints = false
            view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
            view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
            view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
            view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
        }
    }
    

    And here is an example of MyCustomClass that implements the protocol (with the .xib file being named MyCustomClass.xib):

    @IBDesignable
    class MyCustomClass: UIView, NibLoadable {
    
        @IBOutlet weak var myLabel: UILabel!
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setupFromNib()
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupFromNib()
        }
    
    }
    

    NOTE: If you miss the Gotcha and set the class value inside your .xib file to be your custom class, then it will not draw in the storyboard and you will get a EXC_BAD_ACCESS error when you run the app because it gets stuck in an infinite loop of trying to initialize the class from the nib using the init?(coder aDecoder: NSCoder) method which then calls Self.nib.instantiate and calls the init again.

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

    Assuming that you've created an xib that you want to use:

    1) Create a custom subclass of UIView (you can go to File -> New -> File... -> Cocoa Touch Class. Make sure "Subclass of:" is "UIView").

    2) Add a view that's based on the xib as a subview to this view at initialization.

    In Obj-C

    -(id)initWithCoder:(NSCoder *)aDecoder{
        if (self = [super initWithCoder:aDecoder]) {
            UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:@"YourXIBFilename"
                                                                  owner:self
                                                                options:nil] objectAtIndex:0];
            xibView.frame = self.bounds;
            xibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
            [self addSubview: xibView];
        }
        return self;
    }
    

    In Swift 2

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        let xibView = NSBundle.mainBundle().loadNibNamed("YourXIBFilename", owner: self, options: nil)[0] as! UIView
        xibView.frame = self.bounds
        xibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
        self.addSubview(xibView)
    }
    

    In Swift 3

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        let xibView = Bundle.main.loadNibNamed("YourXIBFilename", owner: self, options: nil)!.first as! UIView
        xibView.frame = self.bounds
        xibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        self.addSubview(xibView)
    }
    

    3) Wherever you want to use it in your storyboard, add a UIView as you normally would, select the newly added view, go to the Identity Inspector (the third icon on the upper right that looks like a rectangle with lines in it), and enter your subclass's name in as the "Class" under "Custom Class".

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

    I've always found the "add it as a subview" solution unsatisfactory, seeing as it screws with (1) autolayout, (2) @IBInspectable, and (3) outlets. Instead, let me introduce you to the magic of awakeAfter:, an NSObject method.

    awakeAfter lets you swap out the object actually woken up from a NIB/Storyboard with a different object entirely. That object is then put through the hydration process, has awakeFromNib called on it, is added as a view, etc.

    We can use this in a "cardboard cut-out" subclass of our view, the only purpose of which will be to load the view from the NIB and return it for use in the Storyboard. The embeddable subclass is then specified in the Storyboard view's identity inspector, rather than the original class. It doesn't actually have to be a subclass in order for this to work, but making it a subclass is what allows IB to see any IBInspectable/IBOutlet properties.

    This extra boilerplate might seem suboptimal—and in a sense it is, because ideally UIStoryboard would handle this seamlessly—but it has the advantage of leaving the original NIB and UIView subclass completely unmodified. The role it plays is basically that of an adapter or bridge class, and is perfectly valid, design-wise, as an additional class, even if it is regrettable. On the flip side, if you prefer to be parsimonious with your classes, @BenPatch's solution works by implementing a protocol with some other minor changes. The question of which solution is better boils down to a matter of programmer style: whether one prefers object composition or multiple inheritance.

    Note: the class set on the view in the NIB file remains the same. The embeddable subclass is only used in the storyboard. The subclass can't be used to instantiate the view in code, so it shouldn't have any additional logic, itself. It should only contain the awakeAfter hook.

    class MyCustomEmbeddableView: MyCustomView {
      override func awakeAfter(using aDecoder: NSCoder) -> Any? {
        return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any
      }
    }
    

    ⚠️ The one significant drawback here is that if you define width, height, or aspect ratio constraints in the storyboard that don't relate to another view then they have to be copied over manually. Constraints that relate two views are installed on the nearest common ancestor, and views are woken from the storyboard from the inside-out, so by the time those constraints are hydrated on the superview the swap has already occurred. Constraints that only involve the view in question are installed directly on that view, and thus get tossed when the swap occurs unless they are copied.

    Note that what is happening here is constraints installed on the view in the storyboard are copied to the newly instantiated view, which may already have constraints of its own, defined in its nib file. Those are unaffected.

    class MyCustomEmbeddableView: MyCustomView {
      override func awakeAfter(using aDecoder: NSCoder) -> Any? {
        let newView = (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)!
    
        for constraint in constraints {
          if constraint.secondItem != nil {
            newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newView, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
          } else {
            newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: constraint.constant))
          }
        }
    
        return newView as Any
      }
    }  
    

    instantiateViewFromNib is a type-safe extension to UIView. All it does is loop through the NIB's objects until it finds one that matches the type. Note that the generic type is the return value, so the type has to be specified at the call site.

    extension UIView {
      public class func instantiateViewFromNib<T>(_ nibName: String, inBundle bundle: Bundle = Bundle.main) -> T? {
        if let objects = bundle.loadNibNamed(nibName, owner: nil) {
          for object in objects {
            if let object = object as? T {
              return object
            }
          }
        }
    
        return nil
      }
    }
    
    0 讨论(0)
  • 2020-11-27 10:45

    I think about alternative for using XIB views to be using View Controller in separate storyboard.

    Then in main storyboard in place of custom view use container view with Embed Segue and have StoryboardReference to this custom view controller which view should be placed inside other view in main storyboard.

    Then we can set up delegation and communication between this embed ViewController and main view controller through prepare for segue. This approach is different then displaying UIView, but much simpler and more efficiently (from programming perspective) can be utilised to achieve the same goal, i.e. have reusable custom view that is visible in main storyboard

    The additional advantage is that you can implement you logic in CustomViewController class and there set up all delegation and view preparation without creating separate (harder to find in project) controller classes, and without placing boilerplate code in main UIViewController using Component. I think this is good for reusable components ex. Music Player component (widget like) that is embeddable in other views.

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