Search code examples
iosswiftxcodeuiviewuikit

What is the correct way to load view from xib?


All of the resources I have found so far suggest variations of this code for loading a view from xib file, but always implementing the same logic:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    var view = (Bundle.main.loadNibNamed("MyCustomView", owner: self, options: nil)![0])
    self.addSubview(view as! UIView)
}

Which doesn't look right to me because this function belongs to a subclass of UIView, and adds the intended design (xib's design) as a subview. In this case if we added a different UI element to self, it would belong to a different parent than the UI elements in the xib file.

So the hierarchy would look like this:

---self (UIView)  
    ---Other UI elements added by code in `self`
    ---xib (UIView)
        ---UI elements in xib file

Isn't this an unnecessary wrapping? If so, is there a way to add all the views in xib file to self directly? Or is there a different more elegant way of loading design from a xib file?


Solution

  • One way to reuse a complex set of subviews is to define an embedded view controller. You start by defining your top-level view controller. In it, you define an outlet and connected it to an instance of your sub-controller (also defined in the nib). You also connect the sub-controller's view to a placeholder UIView in the top-level nib's view hierarchy.

    class ViewController: UIViewController {
    
        @IBOutlet var childController: ReusableViewController?
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
        }
    }
    

    The slight-of-hand occurs in the sub-controller. Its awakeFromNib function gets invoked when the super-controller is loaded. The sub-controller then uses the "placeholder" UIView it is connected to to insert it's view hierarchy into the top-level views.

    class ReusableViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
        }
    
        override func awakeFromNib() {
            super.awakeFromNib()
            // This controller manages a reusable view defined by a seperate nib.
            // When defined in the parent nib, its view is connected to a placeholder view object.
            // When the nib is loaded, the secondary nib is loaded and replaces the placeholder.
            let placeholder = self.view!
            Bundle.main.loadNibNamed("ReusableViewController", owner: self, options: nil)
            // (the nib connects the view property to the actual view, replacing the placeholder)
            placeholder.superview?.insertSubview(self.view, aboveSubview: placeholder)
            // (do something about constraints here?)
            placeholder.removeFromSuperview()
        }
    
    }
    

    The advantage to this arrangement is that the sub-controller can have whatever bindings, outlets, actions, connections, properties, and business logic that it needs, neatly encapsulated and reusable.

    This is a bit of a hack because it short-circuits the sub-controller's view-did-load lifecycle (because the controller never loads its own view). To address that, you could define another property that pointed the sub-controller at either a placeholder view or the container view that is should insert itself into. Then replace the Bundle.main.loadNibName(blah, blah, blah) stuff with just let replacement = self.view and let the view controller do all of the loading.


    Here's a finished solution using storyboards contributed by the original poster after getting this to work

    Using the reusable view in storyboard
    Add a Container View (Not to confuse with View) to ViewController (your main view on storyboard), and set it's scene's (controller) class to the controller we just defined (ReusableViewController).
    gif
    That's it. This way you can add as many subviews to a view without having to wrap your subviews in storyboards as well as in code.
    Container View's intended use is actually documented as exactly for this purpose here