Doing it all in code
Programmatic UI in iOS
I’m one of those developers who like to specify the layout of an app’s UI in source code. I use Interface Builder as well, but to a much lesser extent. I think arguments can be made for and against both methods, and it really comes down to personal preferences, priorities and workflows. End of the day, doing it all in code is what I am most comfortable with.
The major disadvantage with doing the layout in code is that setting up the Auto Layout constraints involves writing verbose and “noisy” code, even when using the visual format. To keep code readable, many developers create a thin abstraction over Auto Layout as Objective-C categories or Swift extensions, like Mike Swanson and Justin Driscoll for example. Some even use heavy abstractions like Masonry and PureLayout to address this.
I have my own thin abstraction that works well for me, and I’d like to show you how it works and what my code looks like.
The Auto Layout abstraction layer
My goal for the abstraction was to make the layout code look less imperative and more declarative. With Swift’s terse syntax for enums and tuples, we can express the intended layout with very little code.
For example, let’s say we have this design for a browser’s omnibox:
This design needs three views:
- A background button that spans the whole omnibox to make the whole thing tappable.
- A label at the center to show the hostname
- A reload button at the right edge, maybe with some margin, centered vertically
The abstraction layer provides a addSubviewsWithConstraints
method
that lets me specify this layout as:
The addSubviewsWithConstraints
method takes a variable number of
tuples as arguments. Each tuple is of type (UIView, [LayoutInfo])
,
where LayoutInfo
is an enum with possible values like .FillIn
,
.CenterIn
and .Anchor
.
For every view we pass in, addSubviewsWithConstraints
does the
following:
- Adds the passed view as a subview
- Sets the passed view’s
translatesAutoresizingMaskIntoConstraints
to false (which we need to always do when setting a view’s Auto Layout constraints in code) - Converts the information in
LayoutInfo
into a bunch of Auto Layout constraints and adds the constraints. The views and view controllers used in theLayoutInfo
part should be either the parent or a sibling of the passed view.
The resulting layout code is readable, informative and terse. As always, we need to ensure that the constraints we specify are not ambiguous or conflicting.
The source code that enables this abstraction can be found here: AutoLayoutHelper.swift. This code is still Swift 1.2, but it can be ported to Swift 2 with trivial changes.
This abstraction makes heavy use of Swift-only features like tuples and enum associated values. So, unfortunately, it can’t be used for writing UI layouting code in Objective-C.
Views
When I create a custom view with encapsulated subviews, I setup the view
hierarchy in the view’s init
.
The code looks something like this:
A few things to keep in mind:
-
The designated initializer for
UIView
isinit(frame:)
; we need to make sure we call that onsuper
in ourinit
. (The Swift compiler will ask you overrideinit(coder:)
as well - just take Xcode’s Fix-it suggestion to resolve the error.) -
Sometimes, the view knows best what its width and/or height should be, and it’s best to write that down in the view’s code in
intrinsicContentSize
rather than in it’s superview’s Auto Layout constraints code.The Auto Layout system can sometimes come up with a layout where view’s size exceeds the intrinsic size, where it treats the intrinsic size as a minimum (rather than the exact) size. If you encounter this, you will need to set a high content hugging priority with a call to
setContentHuggingPriority
ininit
. -
For any subviews that need to be laid out manually, we can set their frame in
layoutSubviews
. If we are using Auto Layout for some other subviews, we need to make sure we callsuper.layoutSubviews()
, so that the Auto Layout constraints are applied on those subviews.
View Controllers
For view controllers, I setup the view hierarchy in loadView
.
The code looks something like this:
If there are child view controllers, they should be setup along with the
call to addSubviewsWithConstraints
, like:
Some developers who setup the view hierarchy in code do so in
viewDidLoad
. That would work as well. However, viewDidLoad
is
provided as an override point for performing additional setup after
the Interface-Builder-specified view hierarchy is loaded. loadView
is
the intended override point for setting up the view hierarchy purely in
code, and I like to stick to the intended purposes for these override
points.
Things to keep in mind here:
-
In case we write an
init
in our view controller, we need to make sure we call the designated initializer of the superclass like this:super.init(nibName: nil, bundle: nil)
. (The Swift compiler will ask you overrideinit(coder:)
as well - just take Xcode’s Fix-it suggestion to resolve the error.) -
If we have to access a layout guide when setting up the layout, we should populate
self.view
before we can do that. That’s why we have aself.view = v
at the start ofinit
here. -
This is obvious, but sometimes overlooked: In
loadView
, you should never read theview
property before assigning to it because doing that will just callloadView
again. All you get is an infinite recursion.On the contrary, if you’re setting up the view hierarchy in
viewDidLoad
, you might want to only read theview
property and not write to it.
Animations
Animations with Auto Layout need special attention because though we’ve constrained the layout of the views, we would generally want those constraints to be not upheld accurately for the duration of the animation.
-
Simple movements
For simple horizontal or vertical movements, that constraint can be specified separately (i.e. not within an
addSubviewsWithConstraints
), so that its constant can be modified later on.For example, to be able to move the
hostnameLabel
horizontally, I can do something like:self.addSubviewsWithConstraints( (hostnameLabel, [ .CenterVerticallyIn(self) // Left anchoring done separately ]) ) var layoutConstraint = NSLayoutConstraint( item: hostnameLabel, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: 0) self.addConstraint(layoutConstraint) self._leftAnchorConstraint = layoutConstraint
and later, modify the constant in an animation block, like:
self.layoutIfNeeded() // Flush pending layout operations UIView.animateWithDuration(0.4, animations: { self._leftAnchorConstraint?.constant = updatedValue self.layoutIfNeeded() } )
-
Animations involving transforms
When the animation involves view transforms, I like to switch completely to manual layout during the animation, and hook up the Auto Layout constraints, if applicable, after the animation is over.
If the view is created and animated into a view hierarchy, I create it as manually placed without any constraints, do the animation, and setup Auto Layout constraints in the animation’s completion block.
The abstraction layer provides an
addLayoutConstraintsForSubview
method for adding constraints for a previously added subview.// Animating the appearance of `newlyCreatedView` let subview: UIView = newlyCreatedView subview.transform = initialTransform subview.center = initialCenter UIView.animateWithDuration(0.4, delay: 0, options: .CurveEaseIn, animations: { subview.transform = finalTransform subview.center = finalCenter }, completion: { subview.translatesAutoresizingMaskIntoConstraints = false self.view.addLayoutConstraintsForSubview( subview, [ .FillIn(self.view) ] ) } )
If the view is animating out of a view hierarchy and is to be removed from the screen, I remove all constraints and switch to manual layout before the animation starts.
The abstraction layer provides a
removeLayoutConstraintsForSubview
method to help in switching to manual layout without removing the subview from the view hierarchy.// Animating the disappearance of `subview` self.view.removeLayoutConstraintsForSubview(subview) subview.translatesAutoresizingMaskIntoConstraints = true self.view.layoutIfNeeded() subview.transform = initialTransform subview.center = initialCenter UIView.animateWithDuration(0.4, delay: 0, options: .CurveEaseIn, animations: { subview.transform = finalTransform subview.center = finalCenter }, completion: { subview.removeFromSuperview() } )
If, instead of disappearing, the view should move to a new view hierarchy, we can use either
addSubviewsWithConstraints
oraddLayoutConstraintsForSubview
in the completion block to setup the view hierarchy after the animation is over.
What’s your take?
This is what I use, and it works well for me. What strategies and tricks do you use when setting up the UI in code?
Comments and feedback welcome on Twitter @roopeshchander.