In this post we will build a Sign In and Password Recovery form with a single flexible layout, using Swift and the UIStackView class, which has been available since the release of the iOS 9 SDK. By taking advantage of UIStackView's properties, we will dynamically adapt to the device's orientation and show / hide different form components with animations. The source code for this post can the found in this github repository.
Auto Layout has become a requirement for any application that wants to adhere to modern best practices of iOS development. When introduced in iOS 6, it was optional and full visual support in Interface Builder just wasn't there. With the release of iOS 8 and the introduction of Size Classes, the tools and the API improved but you could still dodge and avoid Auto Layout. But now, we are at a point where, in order to fully support all device sizes and split-screen multitasking on the iPad, you must embrace it and design your applications with a flexible UI in mind.
Auto Layout basically works as an linear equation solver, taking all of the constraints defined in your views and subviews, and calculates the correct sizes and positioning for them. One disadvantage of this approach is that you are obligated to define, typically, between 2 to 6 constraints for each control you add to your view. With different constraint sets for different size classes, the total number of constraints increases considerably and the complexity of managing them increases as well.
In order to reduce this complexity, the iOS 9 SDK introduced the UIStackView, an interface control that serves the single purpose of laying out collections of views. A UIStackView will dynamically adapt its containing views' layout to the device's current orientation, screen sizes and other changes in its views. You should keep the following stack view properties in mind:
Apple recommends that you use UIStackView for all cases, as it will seriously reduce constraint overhead. Just be sure to judiciously use compression and content hugging priorities to solve possible layout ambiguities.
The sample application we'll build features a simple Sign In form, with the option for recovering a forgotten password, all in a single screen.
When tapping on the "Forgot your password?" button, the form will change, hiding the password text field and showing the new call-to-action buttons and message labels. By canceling the password recovery action, these new controls will be hidden once again and the form will return to it's initial state.
This is what the form will look like when we're done. Let's start by creating a new iOS > Single View Application template.
Then, we add a new UIStackView to the ViewController and add some constraints for positioning it within its parent view. Since we want a full screen width vertical form, we set its axis to .Vertical, the alignment to .Fill and the distribution to .FillProportionally, so that individual views within the stack view can grow bigger or smaller, according to their content.
class ViewController : UIViewController
{
let formStackView = UIStackView()
...
override func viewDidLoad() {
super.viewDidLoad()
// Initialize the top-level form stack view
formStackView.axis = .Vertical
formStackView.alignment = .Fill
formStackView.distribution = .FillProportionally
formStackView.spacing = 8
formStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(formStackView)
// Anchor it to the parent view
view.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat("H:|-20-[formStackView]-20-|", options: [.AlignAllRight,.AlignAllLeft], metrics: nil, views: ["formStackView": formStackView])
)
view.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat("V:|-20-[formStackView]-8-|", options: [.AlignAllTop,.AlignAllBottom], metrics: nil, views: ["formStackView": formStackView])
)
...
}
...
}
Next, we'll add all the fields and buttons that make up our form. We'll only present a couple of them here as the rest of the code is boilerplate. In order to refrain UIStackView from growing the height of our inputs and buttons as needed to fill vertical space, we add height constraints to set the maximum value for their vertical size.
class ViewController : UIViewController
{
...
var passwordField: UITextField!
var signInButton: UIButton!
var signInLabel: UILabel!
var forgotButton: UIButton!
var backToSignIn: UIButton!
var recoverLabel: UILabel!
var recoverButton: UIButton!
...
override func viewDidLoad() {
...
// Add the email field
let emailField = UITextField()
emailField.translatesAutoresizingMaskIntoConstraints = false
emailField.borderStyle = .RoundedRect
emailField.placeholder = "Email Address"
formStackView.addArrangedSubview(emailField)
// Make sure we have a height constraint, so it doesn't change according to the stackview auto-layout
emailField.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat("V:[emailField(<=30)]", options: [.AlignAllTop, .AlignAllBottom], metrics: nil, views: ["emailField": emailField])
)
// Add the password field
passwordField = UITextField()
passwordField.translatesAutoresizingMaskIntoConstraints = false
passwordField.borderStyle = .RoundedRect
passwordField.placeholder = "Password"
formStackView.addArrangedSubview(passwordField)
// Make sure we have a height constraint, so it doesn't change according to the stackview auto-layout
passwordField.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat("V:[passwordField(<=30)]", options: .AlignAllCenterY, metrics: nil, views: ["passwordField": passwordField])
)
...
}
...
}
By taking advantage of the previously mentioned properties of UIStackView, we can transition from the Sign In form to the Password Recovery form by showing and hiding specific field and buttons. We do this by setting the hidden property within a UIView.animateWithDuration block.
class ViewController : UIViewController
{
...
// Callback target for the Forgot my password button, animates old and new controls in / out
func forgotTapped(sender: AnyObject) {
UIView.animateWithDuration(0.2) { [weak self] () -> Void in
self?.signInButton.hidden = true
self?.signInLabel.hidden = true
self?.forgotButton.hidden = true
self?.passwordField.hidden = true
self?.recoverButton.hidden = false
self?.recoverLabel.hidden = false
self?.backToSignIn.hidden = false
}
}
// Callback target for the Back to Sign In button, animates old and new controls in / out
func backToSignInTapped(sender: AnyObject) {
UIView.animateWithDuration(0.2) { [weak self] () -> Void in
self?.signInButton.hidden = false
self?.signInLabel.hidden = false
self?.forgotButton.hidden = false
self?.passwordField.hidden = false
self?.recoverButton.hidden = true
self?.recoverLabel.hidden = true
self?.backToSignIn.hidden = true
}
}
...
}
Because we have many vertical input fields and buttons, space can become an issue when presenting in a compact vertical size, like the iPhone in landscape. To overcome this, we add a stack view to the header section of the form and change its axis orientation between Vertical and Horizontal, according to the current active size class.
override func viewDidLoad() {
...
// Initialize the header stack view, that will change orientation type according to the current size class
headerStackView.axis = .Vertical
headerStackView.alignment = .Fill
headerStackView.distribution = .Fill
headerStackView.spacing = 8
headerStackView.translatesAutoresizingMaskIntoConstraints = false
...
}
// If we are presenting in a Compact Vertical Size Class, let's change the header stack view axis orientation
override func willTransitionToTraitCollection(newCollection: UITraitCollection, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if newCollection.verticalSizeClass == .Compact {
headerStackView.axis = .Horizontal
} else {
headerStackView.axis = .Vertical
}
}
So, with a couple of UIStackViews, we've built a flexible form only by defining a few height constraints for our input fields and buttons, with all the remaining constraints magically managed by the stack views. Here is the end result:
We have included in the sample source code a view controller with this same example but designed with Interface Builder. There, you can clearly see that we have less than 10 constraints, on a layout that could easily have up to 40-50 constraints if we had not used UIStackView. Stack Views are here to stay and you should use them now if you are targeting iOS 9 and above.
Milton Moura (@mgcm) is a freelance iOS developer based in Portugal. He has worked professionally in several industries, from aviation to telecommunications and energy and is now fully dedicated to creating amazing applications using Apple technologies. With a passion for design and user interaction, he is also very interested in new approaches to software development. You can find out more at http://defaultbreak.com