Skip to content
scottybe edited this page Mar 6, 2018 · 10 revisions

Overview

View objects encapsulate logic to display information on the screen and respond to user events. UIKit comes with a [catalog of views][viewcatalog] that can be used to build many kinds of user interfaces. However, you can also define your own [custom view][applecustomview] classes. You might want to create a custom view: [viewcatalog]: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/ [applecustomview]: https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/CreatingViews/CreatingViews.html#//apple_ref/doc/uid/TP40009503-CH5-SW23

  1. to create a reusable component that appears in many places in your application(s). The view can encapsulate both the visual appearance and the behavior of the component.
  2. to do customized drawing logic. You must create a subclass UIView in order to override drawRect.
  3. to do low-level event handling since UIView is a subclass of UIResponder. This is rare. Gesture recognizers are generally preferred because they can be reused since they do not couple the low-level event handling logic with the view.

How views are defined and instantiated

In order to define our own custom views, we need to understand how views are defined and created. Each view is an instance of some subclass of UIView. The class is responsible for defining the behavior of the view. It may also be responsible for the layout and visual appareance of the view.

However, the layout and visual appearance of a view may be described in a separate file created with Interface Builder (a .xib file). We'll refer to any Interface Builder file as a nib file—the naming here is for historical reasons.

NB: Many of the things we describe for nibs will also apply to storyboards, which are essentially nibs that can contain segues and can only have view controllers at top-level objects. In particular, the process by which a storyboard instantiates its objects is mostly the same.

Views are generally created in one of two ways:

  1. You can programatically instantiate a view by calling initWithFrame. This is generally done in a view contoller's viewDidLoad method or in code that is responding to some event. In this case you will need to manually add the view to the view hierarchy.

  2. If a nib includes (possibly as a subview) a view of some (possibly custom) class, then when the nib is loaded, an instance of that class will be created by calling initWithCoder. If the view was a subview then it will automatically be inserted into the top-level view's hierarchy. If the view was a top-level view, then you will have manually add the view to the view hierarchy.

If we want our custom views to support both use cases we'll have to override both initWithFrame and initWithCoder in custom view classes.

Example: an image view with caption

To illustrate the different situations we'll come across when working with custom views, we will implement a simple example. Suppose our application has many places where we need to display an image with a caption. We'll create a custom view CaptionableImageView that contains a image view with a caption over a translucent gray background.

This is an example of encapsulating a reusable component with a custom view. For example, in a production application, you might add more functionality to this class to allow customization of where the caption is positioned or to make the caption disappear when the user taps on the image.

Using custom views defined programatically

We can define both the appearance and behavior of our custom view programatically in the CaptionableImageView class as follows.

class CaptionableImageView: UIView {
    var label: UILabel!
    var imageView: UIImageView!

    var caption: String? {
        get { return label?.text }
        set { label.text = newValue }
    }

    var image: UIImage? {
        get { return imageView.image }
        set { imageView.image = newValue }
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initSubviews()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initSubviews()
    }

    func initSubviews() {
        // sets the image's frame to fill our view
        imageView = UIImageView(frame: bounds)
        imageView.contentMode = UIViewContentMode.ScaleAspectFill
        imageView.clipsToBounds = true
        addSubview(imageView)

        // caption has translucent grey background 30 points high and span across bottom of view
        let captionBackgroundView = UIView(frame: CGRectMake(0, bounds.height - 30, bounds.width, 30))
        captionBackgroundView.backgroundColor = UIColor(white: 0.1, alpha: 0.8)
        addSubview(captionBackgroundView)

        label = UILabel(frame: captionBackgroundView.bounds.rectByInsetting(dx: 10, dy: 5))
        label.textColor = UIColor(white: 0.9, alpha: 1.0)
        captionBackgroundView.addSubview(label)
    }
}

Note that both our initWithCoder and initWithFrame methods call another method initSubviews that does the real initialization work. This is a common pattern when you need to create a custom view can be used both programatically and within a nib.

Programmatic instantiation

To use this programatically is fairly straightforward. We simply instantiate the view and add it as subview.

class ViewController: UIViewController {

    var imageView: CaptionableImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // we'd probably want to set up constraints here in a real app
        imageView = CaptionableImageView(frame: CGRectMake(0, 20, view.bounds.width, 200))
        imageView.image = UIImage(named: "yodawg")
        imageView.caption = "Yo dawg, I heard you like views"
        view.addSubview(imageView)
    }
}

Embedding in a nib or storyboard

To use our custom view inside a nib we simply drag in a View (colored orange below for visibility) from the Object Library and set the view's custom class in the attributes inspector.

Here we've added the custom view to our main view controller in the storyboard. We can get a reference to this view as we would any other by creating an @IBOutlet. You can verify that the initWithCoder method is called for CaptionableImageView when storyboard is loaded by adding a breakpoint.

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var imageView: CaptionableImageView!

    override func viewDidLoad() {
        super.viewDidLoad()
        imageView.image = UIImage(named: "yodawg")
        imageView.caption = "Yo dawg, I heard you like views"
    }
}

Nibs and how they are loaded

In order to work with custom views whose layout is defined in a nib, we need to learn more about what exactly a nib is and how it is are loaded.

Nibs declaratively define the layout and configuration of objects in your application—most of the time you'll use them to configure views and view controllers, but arbitrary objects can be configured in Interface Builder by dragging in an Object item from the Object Library.

Nibs also contain information about how these objects are related to each other. In particular, a nib can set a property on an object to point to another object via outlets.

The file's owner object

Of particular importance is the ability to have outlets to and from objects that are not defined inside the nib. This means that you can have a nib define references to and from an object that does not have to be provided until the nib is loaded. For example a view object might have a delegate property that needs to be bound to an object that is created by your application at runtime. Or alternatively, your custom class might need a reference to a label defined inside the nib.

Interface Builder allows you to accomplish these things by providing a placeholder file's owner object. You can define outlets from the file's owner to objects in your nib by setting its custom class and then using the assistant editor and control dragging objects into the custom class.

For example in our CaptionableImageView example we might define our label and image view inside the nib and add outlets for them in the file's owner:

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 nib. In particular, creating an instance of your custom class will not load elements from the nib for you, and vice-versa loading the nib will not create an instance of the custom class for you.

Nib loading process

A nib can be manually loaded by creating an instance of UINib and then calling instantiateWithOwner.

        // nil here means use the default main bundle
        let nib = UINib(nibName: "YourNibName", bundle: nil)

        // objects is an array of all top-level objects in the nib
        let objects = nib.instantiateWithOwner(yourOwnerObject, options: nil)

Two important things happen when instantiateWithOwner is called:

  1. Every object described in the nib is initialized. In particular, any view objects will have an instance of their (possibly custom) class created via initWithCoder.
  2. Any property connected by an outlet is set to the appropriate object. In particular, any outlets on file's owner will be set on the owner object you passed in.

For example, if our nib was defined to have imageView and label outlets on the file's owner object as above, we might load it as follows

        let captionableImageView = CaptionableImageView(frame: CGRectMake(0, 20, view.bounds.width, 200))
        let nib = UINib(nibName: "CaptionableImageView", bundle: nil)
        let objects = nib.instantiateWithOwner(captionableImageView, options: nil)

        // in this case the only top-level object is the top level view
        captionableImageView.addSubview(objects.first as UIView)

        captionableImageView.imageView.image = UIImage(named: "yodawg")
        captionableImageView.label.text = "Yo dawg, I heard you like views"

A few things to note here:

  • We need to instantiate a CaptionableImageView separately to serve as our file's owner. This means that that the imageView and label properties on captionableImageView will be set to the corresponding image view and label described in the nib.

  • We could have used any other object with key-value coding compliant properties imageView and label as the owner and its properties would be set to the corresponding objects in the nib. If we pass in an object that does not contain one of these properties we will have a runtime error. This is the source of the common error

... 'NSUnknownKeyException', reason: '[<...> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key ...

  • The objects returned to us are not part of any view hierarchy. Here we have to add the top-level UIView to as a subview of our CaptionableImageView.

Using custom views defined in a nib

In this section we'll reimplement our example custom view to load its layout from a nib.

First we create a subclass of CaptionableImageView of UIView as before. We'll then need to create our nib by selecting File -> New -> File... -> iOS -> User Interface -> View. It is customary to give this file the same name as your class, so we'll also name it CaptionableImageView (the `.xib extension gets added automatically).

We can now open the nib and add the image view, label background, and label to our top-level view in interface builder. In this case we'll also add auto layout constraints so that our image expands with with the top-level view and the label and label background are pinned to the bottom of the top-level view.

As above, we set the file's owner's custom class to CaptionableImageView and create outlets for the image view and label. We'll create an additional outlet to the top level view called contentView. The reason for this will be apparent soon.

Finally we add the code for our CaptionableImageView class as follows

class CaptionableImageView: UIView {

    @IBOutlet var contentView: UIView!
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var imageView: UIImageView!

    var caption: String? {
        get { return label?.text }
        set { label.text = newValue }
    }

    var image: UIImage? {
        get { return imageView.image }
        set { imageView.image = newValue }
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initSubviews()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initSubviews()
    }

    func initSubviews() {
        let nib = UINib(nibName: "CaptionableImageView", bundle: nil)
        nib.instantiateWithOwner(self, options: nil)
        contentView.frame = bounds
        imageView.contentMode = UIViewContentMode.ScaleAspectFill
        imageView.clipsToBounds = true
        addSubview(contentView)
    }
}

Regardless of being instantiated either from initWithCoder or initWithFrame, we load the nib with this CaptionableImageView as the owner. This will set the label and imageView properties to point to the ones described in the nib.

It will also set contentView to the only top level-view in our nib. Note however that the top-level objects returned from instantiateWithOwner are not added to any view hierarchy. Therefore we add contentView as a subview of our CaptionableImageView and tell it to take up all the space available to us.

NB: In this case, contentView is actually the same as the first object in the array returned by instantiateWithOwner. However creating an outlet for it is slightly safer and allows us to define more than one top-level view in the nib.

Rendering custom view in Interface Builder

Custom views added inside another nib or storyboard will not render in the InterFace Builder canvas by default. If we want our custom views to appear like any other UIKit view while we design in Interface Builder we just need to set the custom view class to be @IBDesignable and then initialize or nib with bundle, Bundle(for: type(of: self))

Modify the above snippet like this to get your custom view rendering in Interface Builder canvass:

// Tell Interface Builder to render in storyboard canvas
@IBDesignable
class CaptionableImageView: UIView {
...
    func initSubviews() {
        // Set bundle
        let nib = UINib(nibName: "CaptionableImageView", bundle: Bundle(for: type(of: self)))   
    ...
    }
...
}

Loading programatically

How we use the CaptionableImageView actually remains exactly the same as before when we defined it completely programatically. We call initWithFrame and the logic inside the CaptionableImageView itself handles the loading of the nib and management of its internal view hierarchy for us.

class ViewController: UIViewController {
    var imageView: CaptionableImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // we'd probably want to set up constraints here in a real app
        imageView = CaptionableImageView(frame: CGRectMake(0, 20, view.bounds.width, 200))
        imageView.image = UIImage(named: "yodawg")
        imageView.caption = "Yo dawg, I heard you like views"
        view.addSubview(imageView)
    }
}

Within a another nib or storyboard

Again the procedure to use the CaptionableImageView inside another nib or storyboard remains exactly the same as before when we defined the custom view programatically. We add a View object to our nib/storyboard in Interface Builder and set its custom class to CaptionableImageView. When this nib/storyboard is loaded it will call initWithCoder which will load CaptionableImageView.xib and set up the internal view hierarchy and outlets for us.

Why you shouldn't set top-level view's custom class

Recall from above that when instantiateWithOwner is called on a nib, any view in the nib will be instantiated by calling initWithCoder on that view's (possibly custom) class. You might be wondering then why we don't simply set the custom class of the top-level view to be our custom class CaptionableImageView. This would save us the trouble of having to mess about with "file's owner".

In fact we can do this if we manually load the nib everywhere we plan to use CaptionableImageView. For example suppose we removed all the outlets from file's owner, set the top-level view's custom class to CaptionableImageView and recreated the outlets for imageView and label:

The code for CaptionableImageView looks like:

class CaptionableImageView: UIView {

    @IBOutlet var label: UILabel!
    @IBOutlet var imageView: UIImageView!

    var caption: String? {
        get { return label?.text }
        set { label.text = newValue }
    }

    var image: UIImage? {
        get { return imageView.image }
        set { imageView.image = newValue }
    }
}

We can then load this view in view controller by instantiating the nib and extract the first object. We can pass in nil as file's owner meaning don't set any outlets on file's owner.

class ViewController: UIViewController {

    var imageView: CaptionableImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let nib = UINib(nibName: "CaptionableImageView", bundle: nil)
        let objects = nib.instantiateWithOwner(nil, options: nil)
        imageView = objects.first as CaptionableImageView
        imageView.image = UIImage(named: "yodawg")
        imageView.caption = "Yo dawg, I heard you like views"
        view.addSubview(imageView)
    }
}

This works perfectly fine except for the fact that we now have to load the nib CaptionableImageView.xib everywhere we want to use CaptionableImageView. In particular it is not possible to embed CaptionableImageView as a subview in another nib/storyboard. This is because when the initWithCoder method is called on CaptionableImageView during the outer nib's loading process, nothing happens since we are not loading the CaptionableImageView.xib anywhere.

What happens now if we try to add back the logic for loading the nib inside initWithCoder/initWithFrame?

class CaptionableImageView: UIView {
    ...

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initSubviews()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initSubviews()
    }

    func initSubviews() {
        let nib = UINib(nibName: "CaptionableImageView", bundle: nil)
        nib.instantiateWithOwner(self, options: nil)
    }
}

Now suppose we add a view to our storyboard with custom class CaptionableImageView. When our storyboard is loaded it will call our initWithCoder method. This will then try to load the nib in initSubviews. However, since the top-level view in the nib had its custom class set to CaptionableImageView, the nib loading process will then call our initWithCoder method again! We are stuck in an infinite loop if loading the nib triggers loading the same nib again in initWithCoder.

A common pattern to get around this is to set our custom class and bind all the outlets to file's owner as we did above. Then loading the nib in initWithFrame/initWithCoder will not trigger another initWithCoder on our custom class when the top-level view is instantiated.

In other words, do not set the Content View's Custom Class as your UIView subclass in your nib. Leave it blank, i.e., as UIView. Only the File's Owner's Custom Class should be set to your UIVew subclass in your nib.

A note about view controllers

So far we have only discussed using nibs with views. However, something that is relatively common is to instantiate a nib with the file's owner set to a view controller. This allows you bind outlets to elements inside a nib directly to properties inside your view controller.

Perhaps the most common usage of this is in the view controller's initWithNibName method. This will load the nib, instantiate the view controller, and set the view controller's view to be the top-level view instantiated from the nib. It will also bind any outlets on the file's owner object to properties in view controller.

The process by which a view controller gets loaded from a storyboard is similar except that it will call the view controllers initWithCoder method and set up the nib separately. More information on that process can be found here

Clone this wiki locally