UIStackView
When I was a student in Japan,
I worked part-time at a restaurant —
In contrast, iOS developers often have to jump through several conceptual hoops when laying out user interfaces. After all, placing things in an upside-down 2D coordinate system is not intuitive for anyone but geometry geeks; for the rest of us, it’s not that cut and dried.
But wait — what if we could take physical world concepts like gravity and elasticity and appropriate them for UI layouts?
As it turns out,
there has been no shortage of attempts to do so since the early years of
GUIs
and personal computing —
Motif’s Xm
and Swing’s Box
are notable early specimens.
These widgets are often referred to as stack-based layouts,
and three decades later,
they are alive and well on all major platforms,
including Android’s Linear
and CSS flexbox,
as well as Apple’s own NSStack
, UIStack
, and —
new in SwiftUI —
HStack
, VStack
, and ZStack
.
This week on NSHipster,
we invite you to enjoy a multi-course course
detailing the most delicious morsels
of this most versatile of layout APIs: UIStack
.
Hors-d’œuvres 🍱 Conceptual Overview
Stacking layout widgets come in a wide variety of flavors. Even so, they all share one common ingredient: leaning on our intuition of the physical world to keep the configuration layer as thin as possible. The result is a declarative API that doesn’t concern the developer with the minutiæ of view placement and sizing.
If stacking widgets were stoves, they’d have two distinct sets of knobs:
- Knobs that affect the items it contains
- Knobs that affect the stack container itself
Together, these knobs describe how the available space is allotted; whenever a new item is added, the stack container recalculates the size and placement of all its contained items, and then lets the rendering pipeline take care of the rest. In short, the raison d’être of any stack container is to ensure that all its child items get a slice of the two-dimensional, rectangular pie.
Appetizer 🥗 UIStackView Essentials
Introduced in iOS 9,
UIStack
is the most recent addition to the UI control assortment in Cocoa Touch.
On the surface,
it looks similar to its older AppKit sibling, the NSStack
,
but upon closer inspection,
the differences between the two become clearer.
Managing Subviews
In iOS, the subviews managed by the stack view are referred to as the arranged subviews. You can initialize a stack view with an array of arranged subviews, or add them one by one after the fact. Let’s imagine that you have a set of magical plates, the kind that can change their size at will:
let salad Plate = UIView(…)
let appetizer Plate = UIView(…)
let plate Stack = UIStack View(arranged Subviews: [salad Plate, appetizer Plate])
// or
let side Plate = UIView(…)
let bread Plate = UIView(…)
let another Plate Stack = UIStack View(…)
another Plate Stack.add Arranged Subview(side Plate)
another Plate Stack.add Arranged Subview(bread Plate)
// Use the `arranged Subviews` property to retrieve the plates
another Plate Stack.arranged Subviews.count // 2
You can also insert subviews at a specific index:
let charger Plate = UIView(…)
another Plate Stack.insert Arranged Subview(charger Plate, at: 1)
another Plate Stack.arranged Subviews.count // 3
Adding an arranged view using any of the methods above also makes it a subview of the stack view.
To remove an arranged subview that you no longer want around,
you need to call remove
on it.
The stack view will automatically remove it from the arranged subview list.
In contrast,
calling remove
on the stack view will only remove the view passed as a parameter from the arranged subview list,
without removing it from the subview hierarchy.
Keep this distinction in mind if you are modifying the stack view content during runtime.
plate Stack.arranged Subviews.contains(salad Plate) // true
plate Stack.subviews.contains(salad Plate) // true
plate Stack.remove Arranged Subview(salad Plate)
plate Stack.arranged Subviews.contains(salad Plate) // false
plate Stack.subviews.contains(salad Plate) // true
salad Plate.remove From Superview()
plate Stack.arranged Subviews.contains(salad Plate) // false
plate Stack.subviews.contains(salad Plate) // false
Toggling Subview Visibility
One major benefit of using stack views over custom layouts is their built-in support for toggling subview visibility without causing layout ambiguity;
whenever the is
property is toggled for one of the arranged subviews,
the layout is recalculated,
with the possibility to animate the changes inside an animation block:
UIView.animate(with Duration: 0.5, animations: {
plate Stack.arranged Subviews[0].is Hidden = true
})
This feature is particularly useful when the stack view is part of a reusable view such as table and collection view cells; not having to keep track of which constraints to toggle is a bliss.
Now, let’s resume our plating work, shall we? With everything in place, let’s see what can do with our arranged plates.
Arranging Subviews Horizontally and Vertically
The first stack view property you will likely interact with is the axis
property.
Through it you can specify the orientation of the main axis,
that is the axis along which the arranged subviews will be stacked.
Setting it to either horizontal
or vertical
will force all subviews to fit into a single row or a single column,
respectively.
This means that stack views in iOS do not allow overflowing subviews to wrap into a new row or column,
unlike other implementations such CSS flexbox and its flex-wrap
property.
The orientation that is perpendicular to the main axis is often referred to as the cross axis. Even though this distinction is not explicit in the official documentation, it is one of the main ingredients in any stacking algorithm — without it, any attempt at explaining how stack views work will be half-baked.
The default orientation of the main axis in iOS is horizontal; not ideal for our dishware, so let’s fix that:
plate Stack.axis = .vertical
Et voilà!
Entrée 🍽 Configuring the Layout
When we layout views, we’re accustomed to thinking in terms of origin and size. Working with stack views, however, requires us to instead think in terms of main axis and cross axis.
Consider how a horizontally-oriented stack view works.
To determine the width and the x
coordinate of the origin for each of its arranged subviews,
it refers to a set of properties that affect layout across the horizontal axis.
Likewise, to determine the height and the y
coordinate,
it refers to another set of properties that affects the vertical axis.
The UIStack
class provides axis-specific properties to define the layout: distribution
for the main axis, and alignment
for the cross axis.
The Main Axis: Distribution
The position and size of arranged subviews along the main axis is affected in part by the value of the distribution
property,
and in part by the sizing properties of the subviews themselves.
In practice, each distribution option will determine how space along the main axis is distributed between the subviews.
With all distributions,
save for fill
, the stack view attempts to find an optimal layout based on the intrinsic sizes of the arranged subviews.
When it can’t fill the available space, it stretches
the arranged subview with the the lowest content hugging priority.
When it can’t fit all the arranged subviews,
it shrinks the one with the lowest compression resistance priority.
If the arranged subviews share the same value for content hugging and compression resistance,
the algorithm will determine their priority based on their indices.
With that out of the way, let’s take a look at the possible outcomes, starting with the distributions that prioritize preserving the intrinsic content size of each arranged subview:
-
equal
: The stack view gives every arranged subview its intrinsic size alongside the main axis, then introduces equally-sized paddings if there is extra space.Spacing -
equal
: Similar toCentering equal
, but instead of spacing subviews equally, a variably sized padding is introduced in-between so as the center of each subview alongside the axis is equidistant from the two adjacent subview centers.Spacing
In contrast, the following distributions prioritize filling the stack container, regardless of the intrinsic content size of its subviews:
-
fill
(default): The stack view ensures that the arranged subviews fill all the available space. The rules mentioned above apply. -
fill
: Similar toProportionally fill
, but instead of resizing a single view to fill the remaining space, the stack view proportionally resizes all subviews based on their intrinsic content size. -
fill
: The stack view ensures that the arranged views fill all the available space and are all the same size along the main axis.Equally
The Cross Axis: Alignment
The third most important property of UIStack
is alignment
.
Its value affects the positioning and sizing of arranged subviews along the cross axis.
That is, the Y axis for horizontal stacks,
and X axis for vertical stacks.
You can set it to one of the following values for both vertical and horizontal stacks:
-
fill
(default): The stack view ensures that the arranged views fill all the available space on the cross axis. -
leading
/trailing
: All subviews are aligned to the leading or trailing edge of the stack view along the cross axis. For horizontal stacks, these correspond to the top edge and bottom edge respectively. For vertical stacks, the language direction will affect the outcome: in left-to-right languages the leading edge will correspond to the left, while the trailing one will correspond to the right. The reverse is true for right-to-left languages. -
center
: The arranged subviews are centered along the cross axis.
For horizontal stacks, four additional options are available, two of which are redundant:
-
top
: Behaves exactly likeleading
. -
first
: Behaves likeBaseline top
, but uses the first baseline of the subviews instead of their top anchor. -
bottom
: Behaves exactly liketrailing
. -
last
: Behaves likeBaseline bottom
, but uses the last baseline of the subviews instead of their bottom anchor.
Coming back to our plates, let’s make sure that they fill the available vertical space, all while saving the unused horizontal space for other uses — remember, these can shape-shift!
plate Stack.distribution = .fill
plate Stack.alignment = .leading
Palate Cleanser 🍧 Background Color
Another quirk of stack views in iOS is that they don’t directly support setting a background color. You have to go through their backing layer to do so.
plate Stack.layer.background Color = UIColor.white.cg Color
Alright, we’ve come quite far, but have a couple of things to go over before our dégustation is over.
Dessert 🍮 Spacing & Auto Layout
By default,
a stack view sets the spacing between its arranged subviews to zero.
The value of the spacing
property is treated as an exact value for distributions that attempt to fill the available space
(fill
, fill
, fill
),
and as a minimum value otherwise (equal
, equal
).
With fill distributions, negative spacing values cause the subviews to overlap and the last subview to stretch, filling the freed up space.
Negative spacing
values have no effect on equal centering or spacing distributions.
plate Stack.spacing = 2 // These plates can float too!
The spacing property applies equally between each pair of arranged subviews.
To set an explicit spacing between two particular subviews,
use the set
method instead.
When a custom spacing is used alongside the equal
distribution,
it will be applied on all views,
not just the one specified in the method call.
To retrieve the custom space later on, custom
gives that to you on a silver platter.
plate Stack.set Custom Spacing(4, after: salad Plate)
plate Stack.custom Spacing(after: salad Plate) // 4
You can apply insets to your stack view
by setting its is
to true
and assigning a new value to layout
.
plate Stack.is Layout Margins Relative Arrangement = true
plate Stack.layout Margins = UIEdge Insets(…)
Sometimes you need more control over the sizing and placement of an arranged subview. In those cases, you may add custom constraints on top of the ones generated by the stack view. Since the latter come with a priority of 1000, make sure all of your custom constraints use a priority of 999 or less to avoid unsatisfiable layouts.
let constraint = salad Plate.width Anchor.constraint(equal To Constant: 200)
constraint.priority = .init(999)
constraint.is Active = true
For vertical stack views, the API lets you calculate distances from the subviews’ baselines, in addition to their top and bottom edges. This comes in handy when trying to maintain a vertical rhythm in text-heavy UIs.
plate Stack.is Baseline Relative Arrangement = true // Spacing will be measured from the plates' lips, not their wells.
L’addition s’il vous plaît!
The automatic layout calculation that stack views do for us come with a performance cost. In most cases, it is negligible. But when stack views are nested more than two layers deep, the hit could become noticeable.
To be on the safe side, avoid using deeply nested stack views, especially in reusable views such as table and collection view cells.
After Dinner Mint 🍬 SwiftUI Stacks
With the introduction of SwiftUI during last month’s WWDC,
Apple gave us a sneak peek at how we will be laying out views in the months and years to come:
HStack
, VStack
, and ZStack
.
In broad strokes,
these views are specialized stacking views where the main axis is pre-defined for each subtype and the alignment configuration is restricted to the corresponding cross axis.
This is a welcome change that alleviates the UIStack
API shortcomings highlighted towards the end of cross axis section above.
There are more interesting tidbits to go over, but we will leave that for another banquet.
Stack views are a lot more versatile than they get credit for. Their API on iOS isn’t always the most self-explanatory, nor is it the most coherent, but once you overcome these hurdles, you can bend them to your will to achieve non-trivial feats — nothing short of a Michelin star chef boasting their plating prowess.