Youâve got to handle it to Appleâs team on UIKitâââthey took on an intricate architectural problem and solved it rather beautifully. Given the task of creating useable APIs that would help developers create interfaces across a wide multitude of devices with differing resolutionsâââAuto Layout was born and thus matured.
Though the mathematical formula that constraints are born out of wasnât new, wrapping them in APIs for millions of consumers was. Be that as it may, itâs no secret that NSLayoutConstraint is cumbersome, even for Objective-C standards. UIStackView helps immensely with this.
And yet for a long time, any time I used one I felt like a giant n00b đ.
I got that they were powerful, but boy howdy did I frequent the docs each time I slapped one down in code. They are complex in their simplicity. So this week, I thought Iâd share some notes from the battlefield with my time using stack views.
Intrinsic Sizing
This one is so easy to miss for first timers it hurts. Picture this: You become enamored with the world of iOS. The prospect of devân your own app keeps you helplessly awake at night. Determined, you rip through Reddit subs, online tutorials, books and videos until youâve got some knowledge.
A constant emerges from each of them: Auto Layout is necessary, but itâs scary! Stack views are the answer!
And they arenât wrong. But as a green developer, it can be confusing to understand why putting some custom control into a stack view with valid constraints doesnât work. But sometimes it doesnât, and itâs usually because of intrinsic content size, or a lack thereof.
With any stack view distribution, save for .fillEqually, the stack view takes each arranged viewâs intrinsic size when figuring out how to lay things out on its axis. So, if you create a stack view thatâs pinned to the centerY and centerX of a view thatâs housing some custom controlsâââmake sure it knows how to size things with a simple override in the control itself:
override func intrinsicContentSize() -> CGSize
{
return CGSizeMake(200, 40)
}
If we were to go with .fillEqually, intrinsic content size is tossed out in lieu of each view getting resized to fill the stack viewâs axis. This is a scenario where one would need to pin the stack viewâs constraints.
Understanding Distribution. No, Really.
I canât believe this is so hard for my brain to quickly comprehend, but two kids later and getting 3 hour bursts of sleep at nightâââhere we are đ.
Stack views know how to work their magic based on five things at the end of the day, pure and simple:
- Its given axis (vertical or horizontal)
- Its assigned alignment (center, leading, etc.)
- Its spacing, if any (a simple float value)
- Its external constraints (oh hey, I already know those, cool)
- And its assigned distribution (fill, fill equally, or proportionally, center ahhh wait what why are there so many what do they mean ahhhh)
For me, four out of five of those things I just get. And I always have. Guess the odd man out.
Hereâs the thingâââconceptually alignment is easy for our brains to think about. If you are a vertical stack view, then how will things be aligned horizontally? Iâm essentially setting their X value.
And if Iâm a horizontal stack view, then my alignment tells things how to be vertically centered. Again, now Iâm setting their Y value.
Distribution, though itâs named insanely obvious enoughâââis really no different.
If itâs a vertical stack view, then the enum I assign to it will determine how things stretch, size themselves or otherwise fit on a horizontal plane. If itâs a CGRect Iâm making, then this enum is basically supplying the width part. Now, flip flop that for horizontal stack views.
No joke, I had this comment in an app delegate file (always the dumping ground for my commented section of current To Dos âď¸) in a project for some time:
Alignment == an X or Y determination
Distribution == how wide or tall things will be
And also no joke, I almost made a subclass of stack view that looked something like this:
let hStackView = HorizontalStackView()
hStackView.verticalAlignment = .center
hStackView.widthDistribution = .fill
let vStackView = VerticalStackView()
vStackView.horizontalAlignment = .center
vStackView.heightDistribution = .fill
It simply forwarded the assignments to a normal stack viewâs alignment and distribution property. But, simply keeping that in mind made the possible assignments make much more sense:
let stackView = UIStackView() //Horizontal axis by default
//Widths will be stretched to fill, usually one view takes up the majority of the space
stackView.distribution = .fill
//Widths are stretched to fill with the same width
stackView.distribution = .fillEqually
//Widths are stretched to the same size to fill based off of their intrinsic content size, but they scale to keep the same proportions. Think resizing things in Sketch with the lock on.
stackView.distribution = .fillProportionally
//Padding is used to fill out the space horizontally, but generally the views stay the same size
stackView.distribution = .equalSpacing
//Attempts to keep the horizontal centers of each view to remain equally spaced
stackView.distribution = .equalCentering
And then it all makes sense. Except there is one large piece of the puzzle that the above comments leave out.
Resistance Priorities
Most of the time, a stack viewâs arranged sub views likely wonât fill the entire stack view itself. So, if a stack view finds itself in such a trying predicament, it uses a mechanism you likely wrote down to research more about when learning Auto Layout but probably never revisited.
That is, content compression resistance and content hugging priority.
It makes perfect sense too, given that a majority of the controls in an iOS developerâs arsenal all make use of an intrinsic content size. An easy way to wrap ones head around it is this, consider this stack view:
let stackView = UIStackView() //Horizontal axis
stackView.alignment = .center
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.widthAnchor.constraint(equalToConstant: 200).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 200).isActive = true
stackView.centerXAnchor.constraint(equalTo:view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo:view.centerYAnchor).isActive = trueA
Itâs 200 by 200 and in the center of a view. Itâs distribution strategy is to fill things out horizontally. Now, imagine if you will, it has two subviews within it, each 80 by 80.
We want to fill the stack view, but there is this extra 40 points of horizontal space hanging out. So, which one should grow? The one with the lower content hugging priority.
If the scenario was the same, and yet the two views were instead 120 by 120â weâd need to ask ourselves which view should become smaller in width. The one with the lowest compression resistance priority.
One reason you may have gotten lucky (or unlucky depending on how you see it) and missed this is because a stack view will resize a view based on index if all things are equal or ambiguous. If both views had the same values for either compression resistance or content hugging, the stack view tweaks the first view it finds in its arrangedSubviews array. This can lead to some âWhat the, and why?â moments if you didnât know stack views acted in such a manner.
Alas, because we arenât one for ambiguity on this blog, this is easily avoided by one, or two, simple assignments:
let aView = UIView()
//I don't want to grow in width
aView.setContentHuggingPriority(UILayoutPriorityDefaultHigh, for: UILayoutConstraintAxis.horizontal)
//I don't want to shrink in width
aView.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh, for: UILayoutConstraintAxis.horizontal)
Apple really wraps up things nicely in the docs to put the matter at rest (emphasis mine):
When the arranged views do not fit within the stack view, it shrinks the views according to their compression resistance priority. If the arranged views do not fill the stack view, it stretches the views according to their hugging priority.
Bonus Round
And to round things out, letâs finish up with two quick thoughts.
One can build some padding in by way of setting an inset on a stack view:
let stackView = UIStackView()
stackView.layoutMargins = UIEdgeInsetsMake(10, 0, 10, 0)
A without much effort, the top and bottom of your stack view will enjoy 10 points of padding on the top and bottom. Almost. Because it also requires one more assignment to a boolean property:
let stackView = UIStackView()
stackView.layoutMargins = UIEdgeInsetsMake(10, 0, 0, 10)
stackView.isLayoutMarginsRelativeArrangement = true
And then our margins appear as weâd like them to. Now to finish things out,
Stack views are easily used within a scroll view:
let stackView = UIStackView()
scrollView.addSubview(stackView)
//A little helper I use to set top/bottom/leading/trailing constraints to another superview
stackView.constraintsToEdges(on: scrollView)
Ah, but thatâs not enoughâââthough itâs easy to think that it should be. Due to content sizing, you need to get a little explicit sometimes with the stack view to help it understand itâs width. One accomplishes this by pinning the leading and trailing constraints not only to the scroll view itâs in, but also the super view containing the scroll view:
let stackView = UIStackView()
scrollView.addSubview(stackView)
//A little helper I use to set top/bottom/leading/trailing constraints to another superview
stackView.constraintsToEdges(on: scrollView)
stackView.leadingAnchor.constraint(equalTo:view.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo:view.trailingAnchor).isActive = true
And how you have a infinitely adaptable view that creates constraints at runtime, along with a scrollable view that resizes along with it. 2017 is going great đ!
Wrapping Up
Donât ever be discouraged if something simple becomes complicated in execution for you. Sometimes, the best APIs arrive in such a state. Iâve come to heavily rely on stack views wherever I use Auto Layout, and they live up to the billing. Things are easier to make, faster to prototype and effortless to maintain.
Until next time âď¸.