Loading Custom Views in iOS, The Right Way.
This article talks about couple of points, first one is “Popular approach to load an xib file for a custom view” and drawback of this popular approcach and then we will see the “right way to load an xib for a custom view”.
Lets start then,
If you look up the code to load a xib for a custom view then mostly you will come across something similar to this
1. Create an UIView subclass and a corresponding xib file.
2. Set custom class name for the xib same as UIView subclass’ s name created in the step 1.
3. Connect the outlets for the custom view.
Now to load this xib following is the code
Here we first create an instance of “InformationView” and then call “loadView” function on it.
Let see a breakdown for the ”loadView” function
/*Getting the bundle for the xib that we need to load.*/
let bundleName = Bundle(for: type(of: self))
/* Getting the name of the UIView’s class. This will only work if the xib name and its corresponding class name is same.*/
let nibName = String(describing: type(of: self))
/*Getting instance of a xib by providing xib name and bundle name.bundleName — Here you have to specify the bundle of your xib. We are using Bundle class to get the path of the bundle for the xib but if your xib is in the main bundle then you can use “.main” or “nil” also. Passing ‘nil’ here will look for the xib in the main bundle.*/
let nib = UINib(nibName: nibName, bundle: bundleName)
/*Here “instantiate()” method is called on a xib, any view in the xib will be instantiated by calling “initWithCoder” on that view’s (possibly custom) class.Also any property connected by an outlet is set to the appropriate object.This method return an array of top level objects.Here we are selecting only the first top level object.*/
let view = nib.instantiate(withOwner: nil, options: nil).first as! UIView
Note — We can have multiple top level objects in the xib file and we can show them accordingly as per our requirement.
Consider the following case
Here we have two views as top level object and both gets loaded here but we can selecting only one view here as per the availability of some data.
i.e. if we have data available for the user then we can write following code to load the view on left
let view = nib.instantiate(withOwner: nil, options: nil).first as! UIView
Also if no data is available then we can write following code to load the view on right
let view = nib.instantiate(withOwner: nil, options: nil).last as! UIView
Here first and last object is decided on the basis of the order in which views appears in the xib (highlighted in the last image).
/*This line will make sure that view’s autoresizing mask is not translated into Auto Layout constraints.*/
view.translatesAutoresizingMaskIntoConstraints = false
Setting up the top-level view’s custom class approach works most of the time but it has some drawbacks. Lets discuss that
1. We have to load the “InformationView.xib” everywhere we want to use the “InformationView” i.e. To load “InformationView.xib” we have call “loadView” function on the instance of “InformationView” wherever we have used “InformationView”.This is an overhead that could be avoided.
2. It is not possible to embed “InformationView” as a subview in another xib/storyboard. This is because when the “initWithCoder” method is called on “InformationView” during the outer xib’s loading process, nothing happens since we are not loading the “InformationView.xib” anywhere i.e. If we make view’s custom class as “InformationView” in another xib/storyboard then nothing will be shown to user as code to load the “InformationView.xib” is not present in “InformationView.swift” file.
One could think that if absence of code to load the xib is the problem here then let’s add that and fix this but even if you add that code in both the initialisers then also this will not work and instead it will trigger an infinite loop and eventually your will get a crash 😱.
So why this infinite loop ?
Suppose we add a view to our storyboard with custom class set as “InformationView”. After loading, storyboard will call our “initWithCoder” method. This will then try to load the xib in “initSubviews” function. However, since the top-level view in the xib had its custom class set to “InformationView”, the xib loading process will then call our “initWithCoder” method again. Hence an infinite loop as loading a xib triggers loading the same xib again in initWithCoder.
In the context of the code used here as example, following will be the call sequence
1. Storyboard will call the InformationView’s “initwithcoder”.
2. InformationView’s “initwithcoder” will then give a call to InformationView’s “initSubViews”.
3. InformationView’s “initSubViews” has a call to “nib.instantiate(withOwner: nil, options: nil)”.
4. Here Top-level view in the xib had its custom class set to “InformationView” .Hence “nib.instantiate(withOwner: nil, options: nil)” will again give a call to InformationView’s “initwithcoder”.Hence the infinite loop.
So what’s the right way then ?
Proper way to load custom view from xib goes like this
- In the xib, instead of setting custom class of the view as “InformationView”, set “InformationView” as file owner of xib.
Note that changing the file’s owner’s custom class only defines this relationship temporarily to help you and Interface Builder create the outlets. It does not define a runtime relationship between your class and xib. In particular, creating an instance of your custom class will not load elements from the xib for you, and vice-versa loading the xib will not create an instance of the custom class for you.
2. Create and connect an outlet for the top-level view of “InformatioView”.
3. Connect all the outlets of the file owner.
4. Add the code to load the xib in the initialisers.
5. After loading the “InformatioView’s” xib add it as a subview of “InformatioView”.
6. And this is how an instance of “InformationView” is created and can be added as a subview.
Conclusion
In this approach there is no need to load xib explicitly every-time as code to load the xib is present inside “InformationView” class. Also it is possible to embed “InformationView” as a subview in another xib/storyboard as the code to load the xib is present inside both the initialisers of “InformationView” class.
That’s it for now and i hope you find this helpful. Thanks for reading. 😀😀😀
Resource for this article:
https://guides.codepath.com/ios/Custom-Views#how-views-are-defined-and-instantiated