Building Responsive iOS Applications Using Markup
A tutorial on using the UIStackView class in iOS 9 to build responsive mobile applications.
Join the DZone community and get the full member experience.
Join For FreeWorking with auto layout in iOS has historically been difficult. Initially, the platform provided no support for auto layout at all, requiring developers to either hard-code view sizes and positions or write a lot of complex manual layout code. Layout constraints, introduced in iOS 6, were a step in the right direction, but were still cumbersome to use.
With the introduction of the UIStackView
class in iOS 9, it seemed as though Apple finally "got it". Using stack views, it is possible to create complex view hierarchies that automatically adjust to device size and orientation changes without needing to resort to manual size calculations or constraint gymnastics.
Unfortunately, although UIStackView
allows developers to implement auto layout-driven, or "responsive", applications more easily than ever before, it is still not quite as convenient as it could be. As with other view types, stack views must be created either interactively using storyboards or programmatically in code. Other platforms, including Android and Windows, allow developers to create user interfaces declaratively using an XML-based markup language that parallels the view hiearchy of the application. Building an interface in markup makes it easy to visualize the resulting output as well as recognize differences between revisions. It is also a metaphor that many developers are comfortable with, thanks to the ubiquity of HTML and the World Wide Web.
MarkupKit is an open-source project that brings this capability to iOS. Using MarkupKit, developers can declare the structure of a user interface rather than using a GUI builder or writing the UI code by hand. For example, the following markup creates an instance of UILabel
and sets the value of its text
property to "Hello, World!":
<UILabel text="Hello, World!"/>
The output produced by this markup is identical to the output of the following Swift code:
let label = UILabel()
label.text = "Hello, World!"
Simple Stack View Example
Apple's developer documentation includes a collection of tutorials that illustrate how to incorporate auto layout into an iOS application. Two of these examples focus on UIStackView
. The first one, called "Simple Stack View", demonstrates the use of a vertical stack view to automatically arrange a label, image view, and button:
In Apple's example, the developer is instructed to create a storyboard containing the user interface elements using Interface Builder. The same results can be achieved in markup as follows:
<LMLayerView backgroundColor="#ffffff">
<UIStackView axis="vertical" layoutMarginsRelativeArrangement="true" layoutMarginTop="20" layoutMarginBottom="20" spacing="8">
<UILabel text="Flowers" font="body" textAlignment="center"/>
<UIImageView image="flowers" contentMode="scaleAspectFit"
horizontalContentHuggingPriority="250"
verticalContentHuggingPriority="249"
horizontalContentCompressionResistancePriority="750"
verticalContentCompressionResistancePriority="749"/>
<UIButton style="systemButton" normalTitle="Edit"/>
</UIStackView>
</LMLayerView>
The following simple controller code is used to load the markup and provide navigation to the next example:
import UIKit
import MarkupKit
class SimpleStackViewController: UIViewController {
override func loadView() {
view = LMViewBuilder.viewWithName("SimpleStackView", owner: self, root: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Simple Stack View"
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.Plain,
target: nil, action: nil)
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: UIBarButtonItemStyle.Plain,
target: self, action: "next")
edgesForExtendedLayout = UIRectEdge.None
}
func next() {
navigationController!.pushViewController(NestedStackViewController(), animated: true)
}
}
The markup version uses an instance of LMLayerView
as the root element, since this class will automatically create constraints to pin its children to its own bounds. Additionally, the layoutMargins
property of the stack view is used to create the vertical gap at the top and bottom of the view instead of creating explicit offset constraints. Otherwise, the example is identical to the Apple version.
Nested Stack Views
Apple's second stack view example, "Nested Stack Views", demonstrates the use of multiple stack views in varying orientations to create a more complex layout:
Since stack views alone cannot be used to create the layout, several additional constraints are created to maintain a fixed aspect ratio for the image view and to ensure that the form fields are vertically aligned.
The markup for creating the nested stack view example is as follows:
<LMLayerView backgroundColor="#ffffff">
<!-- Root Stack -->
<UIStackView axis="vertical" layoutMarginsRelativeArrangement="true" layoutMarginTop="20" layoutMarginBottom="20" spacing="8">
<!-- Upper Stack -->
<UIStackView axis="horizontal" spacing="8">
<!-- Image View -->
<UIImageView id="imageView" image="square_flowers"
horizontalContentCompressionResistancePriority="48"
verticalContentCompressionResistancePriority="48"/>
<!-- Name Rows Stack -->
<UIStackView axis="vertical" spacing="8">
<!-- First Name Stack -->
<UIStackView axis="horizontal" alignment="firstBaseline" spacing="8">
<UILabel text="First" font="body"
horizontalContentHuggingPriority="251"
verticalContentHuggingPriority="251"/>
<UITextField id="firstNameTextField" placeholder="Enter First Name" font="System 14" borderStyle="roundedRect"
horizontalContentHuggingPriority="48"
horizontalContentCompressionResistancePriority="749"/>
</UIStackView>
<!-- Middle Name Stack -->
<UIStackView axis="horizontal" alignment="firstBaseline" spacing="8">
<UILabel text="Middle" font="body"
horizontalContentHuggingPriority="251"
verticalContentHuggingPriority="251"/>
<UITextField id="middleNameTextField" placeholder="Enter Middle Name" font="System 14" borderStyle="roundedRect"
horizontalContentHuggingPriority="48"
horizontalContentCompressionResistancePriority="749"/>
</UIStackView>
<!-- Last Name Stack -->
<UIStackView axis="horizontal" alignment="firstBaseline" spacing="8">
<UILabel text="Last" font="body"
horizontalContentHuggingPriority="251"
verticalContentHuggingPriority="251"/>
<UITextField id="lastNameTextField" placeholder="Enter Last Name" font="System 14" borderStyle="roundedRect"
horizontalContentHuggingPriority="48"
horizontalContentCompressionResistancePriority="749"/>
</UIStackView>
</UIStackView>
</UIStackView>
<!-- Text View -->
<UITextView text="Notes:" font="body" backgroundColor="#aaaaaa"
verticalContentHuggingPriority="249"/>
<!-- Button Stack -->
<UIStackView axis="horizontal" alignment="firstBaseline" distribution="fillEqually" spacing="8">
<UIButton style="systemButton" normalTitle="Save"/>
<UIButton style="systemButton" normalTitle="Cancel"/>
<UIButton style="systemButton" normalTitle="Clear"/>
</UIStackView>
</UIStackView>
</LMLayerView>
Again, an LMLayerView
is used as the root element and the top/bottom gap is created using the layoutMargins
property of the root stack view, but otherwise the content of the markup example is identical to the storyboard version. The additional constraints are created by the view controller when the view is loaded, as shown below:
import UIKit
import MarkupKit
class NestedStackViewController: UIViewController {
var imageView: UIImageView!
var firstNameTextField: UITextField!
var middleNameTextField: UITextField!
var lastNameTextField: UITextField!
override func loadView() {
view = LMViewBuilder.viewWithName("NestedStackView", owner: self, root: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Nested Stack Views"
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.Plain,
target: nil, action: nil)
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: UIBarButtonItemStyle.Plain,
target: self, action: "next")
edgesForExtendedLayout = UIRectEdge.None
// Create custom constraints
NSLayoutConstraint.activateConstraints([
// Image view aspect ratio
NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.Width,
relatedBy: NSLayoutRelation.Equal, toItem: imageView, attribute: NSLayoutAttribute.Height,
multiplier: 1.0, constant: 0),
// Equal text field widths
NSLayoutConstraint(item: middleNameTextField, attribute: NSLayoutAttribute.Width,
relatedBy: NSLayoutRelation.Equal, toItem: firstNameTextField, attribute: NSLayoutAttribute.Width,
multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: lastNameTextField, attribute: NSLayoutAttribute.Width,
relatedBy: NSLayoutRelation.Equal, toItem: middleNameTextField, attribute: NSLayoutAttribute.Width,
multiplier: 1.0, constant: 0)
])
}
func next() {
...
}
}
Summary
This article provided an introduction to how iOS application development can be simplified using markup. In addition to auto layout, MarkupKit also provides a number of other features for simplifying app development including CSS-like styling and dynamic localization. For more information, please visit the MarkupKit project on GitHub. Additional code samples can be found in the project wiki.
Opinions expressed by DZone contributors are their own.
Comments