简体   繁体   中英

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?

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

Generally when you implement a xib for a view , it always contains all the needed elements for it . but if you have you can try to add the element like this

var view = (Bundle.main.loadNibNamed("MyCustomView", owner: self, options: nil)![0]) as! UIView

var lbl = UILabel()

lbl.frame  = //.....

view.addSubview(lbl)

self.addSubview(view)

You can load the xib of a view during init:

init() {
    super.init(nibName: "ViewController", bundle: Bundle(identifier: "domain.AppName")!)
    self.loadView()
}

Is that what your are looking for?

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 👣

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM