-
Notifications
You must be signed in to change notification settings - Fork 2
Custom Views
- Overview
- How views are defined and instantiated
- Example: an image view with caption
- Using custom views defined programatically
- Nibs and how they are loaded
- Using custom views defined in a nib
- A note about view controllers
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
- 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.
- to do customized drawing logic. You must create a subclass
UIView
in order to overridedrawRect
. - to do low-level event handling since
UIView
is a subclass ofUIResponder
. 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.
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:
-
You can programatically instantiate a view by calling
initWithFrame
. This is generally done in a view contoller'sviewDidLoad
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. -
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.
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.
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.
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)
}
}
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"
}
}
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.
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 associate editor (tuxedo view) 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.
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:
- 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
. - 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(imageView, 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
andlabel
properties oncaptionableImageView
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
andlabel
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 ourCaptionableImageView
.
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 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.
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)
}
}
Again the procedure to use the CaptionableImageView
inside another nib
or storyboard remains exactly teh 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.
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 have now have to
load the 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.
So far we have only discussed using nibs with views. However, we 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