⌘ kean.blog

Let's Build UIStackView

  

UIStackView is a great showcase of Auto Layout – it is built entirely using constraints. It makes it fairly straightforward to build an open-source replacement. Even if you’re not in the business of building one, you may find this article useful because it will give you a better understanding of how UIStackView works under the hood.

Here is my plan. I’ll create an instance of UIStackView, try different configurations, take screenshots, and print the constraints affecting the stack view layout. With this information, re-implementing UIStackView should be as easy as translating those constraints into code. I’ve already done that in a library named Arranged.

The code from this article is available at kean/Arranged.

Introduction #

UIStackView is an abstraction on top of Auto Layout for laying out a collection of views in either a column or a row.

The stack view manages the layout of all the views in its arrangedSubviews property. These views are arranged along the stack view’s axis, based on their order in the arrangedSubviews array. The exact layout varies depending on the stack view’s axis, distribution, alignment, spacing, and other properties.

I’ll use a stack view with the three arranged views with the following intrinsic content sizes:

Intrinsic Content Size
 Subview0.width == 160 Hug:250 CompressionResistance:750
 Subview0.height == 200 Hug:250 CompressionResistance:750
 Subview1.width == 80 Hug:250 CompressionResistance:750
 Subview1.height == 100 Hug:250 CompressionResistance:750
 Subview2.width == 40 Hug:250 CompressionResistance:750
 Subview2.height == 50 Hug:250 CompressionResistance:750

UIStackView.Distribution #

The distribution determines how the stack view lays out its arranged views along its axis. Let’s start with a default UIStackView configuration, which is also one of the easiest to implement:

.fill #

A layout where the stack view resizes its arranged views so that they fill the available space along the stack view’s axis. 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.

Stack view screenshot

Constraints #

Let’s print a full list of constraints inside the stack view using this code:

constraints(for: stackView).forEach { print($0) }

func constraints(for view: UIView) -> [NSLayoutConstraint] {
    var constraints = [NSLayoutConstraint]()
    constraints.append(contentsOf: item.constraints)
    for subview in item.subviews {
        constraints.append(contentsOf: subview.constraints)
    }
    return constraints
}

The constraints get printed in a full format like this:

<NSLayoutConstraint:0x6100000922f0 'UISV-canvas-connection'
   UIStackView:0x7f892be06190.leading == Subview0.leading
   (active, names: content-view-1:0x7f892be0bb70 )>

As you can see stack view sets a specific identifier based on the role that the constraint plays in the layout. Let’s convert the remaining constraints to a more concise format:

UISV-alignment:
 Subview0.top == Subview1.top
 Subview0.top == Subview2.top
 Subview0.bottom == Subview1.bottom
 Subview0.bottom == Subview2.bottom

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == Subview0.top
 Subview0.bottom == StackView.bottom

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

Constraint Categories #

The first two categories of the constraints ensure that the subviews fill the stack view both horizontally (distribution .fill) and vertically (alignment .fill):

  • UISV-alignment constraints align subviews horizontally by making them have equal .bottom and equal .top layout attributes (NSLayoutAttribute). Notice that the stack view pins all the subviews to the first one.
  • UISV-canvas-connection constraints connect subviews to the ‘canvas’ which is a stack view itself. The first and the last subview gets pinned to the .leading and .trailing layout attributes of the stack view respectively. The first subview also gets pinned to the .top and .bottom attributes.

UISV-spacing constraints simply add the same spacing between each of the subsequent subviews. The spacing is defined by the value of the stack view spacing property.

Intrinsic Content Size #

I haven’t defined the stack’s size along any of its axis. In this case, the stack view’s size grows freely in both dimensions, based on its arranged views. So the other remaining constraints that affect the stack view layout are intrinsic content size constraints which I’ve listed earlier. The stack view takes the height of the Subview0 which is the highest with Subview0.height == 200 Hug:250 CompressionResistance:750. The width of the stack view is equal to the combined width of all subviews + all spacings.

As the result, you get a simple but very useful layout. In terms of constraints, this configuration is the simplest one to implement.

.fillEqually #

A layout where the stack view resizes its arranged views so that they fill the available space along the stack view’s axis. The views are resized so that they are all the same size along the stack view’s axis.

Stack view screenshot

UISV-fill-equally:
 Subview1.width == Subview0.width
 Subview2.width == Subview0.width

UISV-alignment:
 Subview0.bottom == Subview1.bottom
 Subview0.bottom == Subview2.bottom
 Subview0.top == Subview1.top
 Subview0.top == Subview2.top

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == Subview0.top
 Subview0.bottom == StackView.bottom

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

This distribution is almost the same as fill distribution. The only different is the new UISV-fill-equally set of constraints. It forces all of the arranged views to have the same width (or height for vertical UIStackView).

.fillProportionally #

A layout where the stack view resizes its arranged views so that they fill the available space along the stack view’s axis. Views are resized proportionally based on their intrinsic content size along the stack view’s axis.

Stack view screenshot

UISV-fill-proportionally:
 Subview0.width == 0.571429*StackView.width priority:999
 Subview1.width == 0.285714*StackView.width priority:998
 Subview2.width == 0.142857*StackView.width priority:997

Hardcoded Width (Custom Constraint):
 StackView.width == 200

UISV-alignment:
 Subview0.bottom == Subview1.bottom
 Subview0.bottom == Subview2.bottom
 Subview0.top == Subview1.top
 Subview0.top == Subview2.top

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == Subview0.top
 Subview0.bottom == StackView.bottom

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

This one is also similar to the fill distribution, but it requires a new UISV-fill-proportionally category of constraints:

UISV-fill-proportionally:
 Subview0.width == 0.571429*StackView.width priority:999
 Subview1.width == 0.285714*StackView.width priority:998
 Subview2.width == 0.142857*StackView.width priority:997

The multipliers (0.571429, 0.285714, etc) are calculated as a proportion of an arranged view intrinsic content width to the combined intrinsic content width of all visible arranged views + size of all spacings.

At first, this layout seems relatively simple to implement. But there are some potential issues. What happens when not all (or none) of the arranged views have an intrinsic content size? What happens when the intrinsic content size of some of the arranged views changes? Well, as to the last question, UIStackView uses a neat private method _intrinsicContentSizeInvalidatedForChildView in which it recalculates constraints’ multipliers. Unfortunately, you can’t do that, and there doesn’t seem to be any other way to implement this.

Also, notice that the priority of the constraints is higher than both content hugging and compression resistance priorities which both effectively get ignored (unless you change those).

I’m not sure why each constraint has a different priority (999, 998, etc). If you have an idea please leave a comment below.

.equalSpacing #

A layout where the stack view positions its arranged views so that they fill the available space along the stack view’s axis. When the arranged views do not fill the stack view, it pads the spacing between the views evenly. If the arranged views do not fit within the stack view, it shrinks the views according to their compression resistance priority.

Stack view screenshot

UISV-distributing-edge:
 H:[Subview0]-(0)-[_UIOLAGapGuide:0x6080001dbc60]
 H:[_UIOLAGapGuide:0x6080001dbc60]-(0)-[Subview1.leading]
 H:[Subview1]-(0)-[_UIOLAGapGuide:0x6080001db8a0]
 H:[_UIOLAGapGuide:0x6080001db8a0]-(0)-[Subview2.leading]

UISV-fill-equally:
 _UIOLAGapGuide:0x6080001db8a0.width == _UIOLAGapGuide:0x6080001dbc60.width

UISV-spacing:
 H:[Subview0]-(>=10)-[Subview1]
 H:[Subview1]-(>=10)-[Subview2]

UISV-alignment:
 Subview0.bottom == Subview1.bottom
 Subview0.bottom == Subview2.bottom
 Subview0.top == Subview1.top
 Subview0.top == Subview2.top

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == Subview0.top
 Subview0.bottom == StackView.bottom

This configuration requires extra spacers (_UIOLAGapGuide) between subsequent subviews. Spacers are pinned to respected subviews by UISV-distributing-edge constraints. All the spacers have the same size thanks to UISV-fill-equally constraints. The UISV-spacing constraints are a bit different from previous distributions too - they now use NSLayoutRelation.greaterThanOrEqual rather than NSLayoutRelation.equal.

.equalCentering #

A layout that attempts to position the arranged views so that they have an equal center-to-center spacing along the stack view’s axis, while maintaining the spacing property’s distance between views. If the arranged views do not fit within the stack view, it shrinks the spacing until it reaches the minimum spacing defined by its spacing property. If the views still do not fit, the stack view shrinks the arranged views according to their compression resistance priority.

Stack view screenshot

UISV-distributing-edge:
 _UIOLAGapGuide:0x6000001dc5c0.leading == Subview0.centerX
 _UIOLAGapGuide:0x6000001dc5c0.trailing == Subview1.centerX
 _UIOLAGapGuide:0x6000001dc2f0.leading == Subview1.centerX
 _UIOLAGapGuide:0x6000001dc2f0.trailing == Subview2.centerX

UISV-fill-equally:
 _UIOLAGapGuide:0x6000001dc2f0.width ==_UIOLAGapGuide:0x6000001dc5c0.width priority:149

UISV-spacing:
 H:[Subview0]-(>=0)-[Subview1]
 H:[Subview1]-(>=0)-[Subview2]

UISV-alignment:
 Subview0.bottom == Subview1.bottom
 Subview0.bottom == Subview2.bottom
 Subview0.top == Subview1.top
 Subview0.top == Subview2.top

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == Subview0.top
 Subview0.bottom == StackView.bottom

Equal centering distribution is similar to the .EqualSpacing because it also uses spacers (_UIOLAGapGuide) between subsequent views. The main difference is how those spacers are used. Instead of pinning spacers to the respective .leading and .trailing layout attributes of the views they are now pinned to the .centerX.

The other difference is that UISV-fill-equally constraints now have a low priority of 149, which is lower than the default compression resistance, which ensures that:

If the arranged views do not fit within the stack view, it shrinks the spacing until it reaches the minimum spacing defined by its spacing property. If the views still do not fit, the stack view shrinks the arranged views according to their compression resistance priority.

The minimum spacing is again achieved by UISV-spacing constraints which all have a required priority.

UIStackView.Alignment #

.fill #

A layout where the stack view resizes its arranged views so that they fill the available space perpendicular to the stack view’s axis.

Stack view screenshot

UISV-alignment:
 Subview0.bottom == Subview1.bottom
 Subview0.bottom == Subview2.bottom
 Subview0.top == Subview1.top
 Subview0.top == Subview2.top

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == Subview0.top
 Subview0.bottom == StackView.bottom

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

This configuration is exactly the same as the very first one: fill distribution.

.leading #

A layout for vertical stacks where the stack view aligns the leading edge of its arranged views along its leading edge.

Stack view screenshot

UISV-alignment:
 Subview0.top == Subview1.top
 Subview0.top == Subview2.top

UISV-spanning-boundary:
 _UILayoutSpacer.top == Subview0.top priority:999.5
 _UILayoutSpacer.bottom >= Subview0.bottom
 _UILayoutSpacer.top == Subview1.top priority:999.5
 _UILayoutSpacer.bottom >= Subview1.bottom
 _UILayoutSpacer.top == Subview2.top priority:999.5
 _UILayoutSpacer.bottom >= Subview2.bottom

UISV-spanning-fit:
 _UILayoutSpacer.height == 0 priority:51

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == Subview0.top
 _UILayoutSpacer.bottom == StackView.bottom

UISV-ambiguity-suppression:
 Subview0.height == 0 priority:25
 Subview1.height == 0 priority:25
 Subview2.height == 0 priority:25

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

That’s a lot of constraints compared to the previous configurations! Let’s figure out what’s going on here.

The stack view added a new auxiliary _UILayoutSpacer view to which it pins all of the arranged views with UISV-spanning-boundary constraints. Notice that all .bottom constraints use .greaterThanOrEqual relation.

Spacer’s height is bounded by UISV-spanning-fit constraint to disambiguate its height. The height of the arranged views also gets disambiguated automatically by stack view for you (UISV-ambiguity-suppression constraints).

Interestingly vertical UISV-canvas-connection constraints pin different views to the canvas this time. The first arranged view gets pinned to the top, while the spacer (_UILayoutSpacer) gets pinned to the bottom.

Now why is that so complicated? Why is layout spacer necessary and is it necessary at all? Couldn’t you just pin all the arranged views to the stack view itself? I think that it might be excessive, but I might be missing something. If you have an idea why it’s implemented this way please share it in the comments.

.center #

A layout where the stack view aligns the center of its arranged views with its center along its axis.

Stack view screenshot

UISV-alignment:
 Subview0.centerY == Subview1.centerY
 Subview0.centerY == Subview2.centerY

UISV-spanning-boundary:
 _UILayoutSpacer.bottom >= Subview0.bottom
 _UILayoutSpacer.bottom >= Subview1.bottom
 _UILayoutSpacer.bottom >= Subview2.bottom
 _UILayoutSpacer.top <= Subview0.top
 _UILayoutSpacer.top <= Subview1.top
 _UILayoutSpacer.top <= Subview2.top

UISV-spanning-fit:
 _UILayoutSpacer.height == 0 priority:51

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == _UILayoutSpacer.top
 _UILayoutSpacer.bottom == StackView.bottom
 StackView.centerY == Subview0.centerY

UISV-ambiguity-suppression:
 Subview0.height == 0 priority:25
 Subview1.height == 0 priority:25
 Subview2.height == 0 priority:25

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

This and the following alignment (.trailing) is very similar to a leading alignment. This particular configuration has an extra UISV-canvas-connection constraint that pins the first arranged view to the .centerY of the stack view, and has slightly different UISV-spanning-boundary and UISV-alignment. But the idea is the same.

.trailing #

A layout for vertical stacks where the stack view aligns the trailing edge of its arranged views along its trailing edge.

Stack view screenshot

UISV-alignment:
 Subview0.bottom == Subview1.bottom
 Subview0.bottom == Subview2.bottom

UISV-spanning-boundary:
 _UILayoutSpacer.top <= Subview0.top
 _UILayoutSpacer.top <= Subview1.top
 _UILayoutSpacer.top <= Subview2.top
 _UILayoutSpacer.bottom == Subview0.bottom priority:999.5
 _UILayoutSpacer.bottom == Subview1.bottom priority:999.5
 _UILayoutSpacer.bottom == Subview2.bottom priority:999.5

UISV-spanning-fit:
 _UILayoutSpacer.height == 0 priority:51

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == _UILayoutSpacer.top
 Subview0.bottom == StackView.bottom

UISV-ambiguity-suppression:
 Subview0.height == 0 priority:25
 Subview1.height == 0 priority:25
 Subview2.height == 0 priority:25

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

.firstBaseline #

A layout where the stack view aligns its arranged views based on their first baseline. This alignment is only valid for horizontal stacks.

Stack view screenshot

UISV-alignment:
 Subview0.firstBaseline == Subview1.firstBaseline
 Subview0.firstBaseline == Subview2.firstBaseline

UISV-spanning-boundary:
 _UILayoutSpacer.top <= Subview0.top
 _UILayoutSpacer.bottom >= Subview0.bottom
 _UILayoutSpacer.top <= Subview1.top
 _UILayoutSpacer.bottom >= Subview1.bottom
 _UILayoutSpacer.top <= Subview2.top
 _UILayoutSpacer.bottom >= Subview2.bottom

UISV-spanning-fit:
 _UILayoutSpacer.height == 0 priority:51

UISV-canvas-fit:
 StackView.height == 0 priority:49

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == _UILayoutSpacer.top
 _UILayoutSpacer.bottom == StackView.bottom

UISV-text-width-disambiguation:
 Subview0.width == 0.333333*StackView.width priority:760
 Subview1.width == 0.333333*StackView.width priority:760
 Subview2.width == 0.333333*StackView.width priority:760

UISV-ambiguity-suppression:
 Subview0.height == 0 priority:25
 Subview1.height == 0 priority:25
 Subview2.height == 0 priority:25

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

The .firstBaseline and the .lastBaseline alignments only make sense for views like UILabel which has some content with actual baselines (like text).

The set of constraints is similar to the previous scenario: it uses an auxiliary spacer, connects the spacer to the stack view, etc. But there is one discrepancy – UISV-text-width-disambiguation constraints. Without these constraints, the layout would be ambiguous – the layout system won’t be able to decide which text container to prioritize. I would say that this is a user error, and it looks like Apple decided to automatically correct it to produce a layout that’s not completely broken. But there is no trace of this behavior neither in the UIStackView documentation nor in headers, and because of that, I decided not to implement these constraints in Arranged.

.lastBaseline #

A layout where the stack view aligns its arranged views based on their last baseline. This alignment is only valid for horizontal stacks.

Stack view screenshot

UISV-alignment:
 Subview0.lastBaseline == Subview1.lastBaseline
 Subview0.lastBaseline == Subview2.lastBaseline

UISV-spanning-boundary:
 _UILayoutSpacer.top <= Subview0.top
 _UILayoutSpacer.bottom >= Subview0.bottom
 _UILayoutSpacer.top <= Subview1.top
 _UILayoutSpacer.bottom >= Subview1.bottom
 _UILayoutSpacer.top <= Subview2.top
 _UILayoutSpacer.bottom >= Subview2.bottom

UISV-spanning-fit:
 _UILayoutSpacer.height == 0 priority:51

UISV-canvas-fit:
 StackView.height == 0 priority:49

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == _UILayoutSpacer.top
 V:[Subview0]-(>=0)-|

UISV-text-width-disambiguation:
 Subview0.width == 0.333333*StackView.width priority:760
 Subview1.width == 0.333333*StackView.width priority:760
 Subview2.width == 0.333333*StackView.width priority:760

UISV-ambiguity-suppression:
 Subview0.height == 0 priority:25
 Subview1.height == 0 priority:25
 Subview2.height == 0 priority:25

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

Misc #

Hiding Subviews #

The stack view automatically updates its layout whenever views are added, removed or inserted into the arrangedSubviews array, or whenever one of the arranged views’s isHidden property changes.

Stack view screenshot

UISV-hiding:
 Subview0.width == 0

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(10)-[Subview2]

UISV-alignment:
 Subview0.bottom == Subview1.bottom
 Subview0.bottom == Subview2.bottom
 Subview0.top == Subview1.top
 Subview0.top == Subview2.top

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 Subview2.trailing == StackView.trailing
 StackView.top == Subview0.top
 Subview0.bottom == StackView.bottom

This one seems easy at first but is actually quite tricky. Hiding subviews is quite simple, UIStackView just adds a single UISV-hiding constraint and recalculates the spacings. What’s more complicated is how UIStackView reacts to isHidden changes and how it perform animations.

The UIStackView promises to do everything automagically for you:

// Animates removing the first item in the stack.
UIView.animateWithDuration(0.25) { () -> Void in
    let firstView = stackView.arrangedSubviews[0]
    firstView.hidden = true
}

What happens here is UIStackView observes isHidden property of the arranged views, cancels its effect, updates constraints, and automatically calls layoutIfNeeded() at the end of the animation block. I’m a little concerned about what kind of trickery with private APIs is used by UIStackView to make this work. I made a couple of attempts to make this work but ultimately decided that this behavior was both confusing and impractical to implement. I went with a simple fully controlled API:

UIView.animateWithDuration(0.33) {
    stackView.setArrangedView(view, hidden: true)
    stackView.layoutIfNeeded()
}

This way some of the responsibilities get delegated to the user of the library, but all the complexities of working with isHidden are gone.

Margins Relative Layout #

A Boolean value that determines whether the stack view lays out its arranged views relative to its layout margins. If true, the stack view will layout its arranged views relative to its layout margins. If false, it lays out the arranged views relative to its bounds.

Stack view screenshot

UISV-alignment:
 Subview0.bottom == Subview1.bottom
 Subview0.bottom == Subview2.bottom
 Subview0.top == Subview1.top
 Subview0.top == Subview2.top

UISV-canvas-connection:
 UIViewLayoutMarginsGuide.leading == Subview0.leading
 UIViewLayoutMarginsGuide.trailing == Subview2.trailing
 UIViewLayoutMarginsGuide.top == Subview0.top
 UIViewLayoutMarginsGuide.bottom == Subview0.bottom

UIView-margin-guide-constraint:
 V:[UIViewLayoutMarginsGuide]-(8)-|
 H:|-(8)-[UIViewLayoutMarginsGuide]
 H:[UIViewLayoutMarginsGuide]-(8)-|
 |-(8)-[UIViewLayoutMarginsGuide]

UISV-spacing:
 H:[Subview0]-(0)-[Subview1]
 H:[Subview1]-(0)-[Subview2]

UISV-canvas-connection constraints now pin subviews to UIViewLayoutMarginsGuide instead of bounds.

Baseline Relative Layout #

A Boolean value that determines whether the vertical spacing between views is measured from their baselines. If YES, the vertical space between views are measured from the last baseline of a text-based view, to the first baseline of the view below it. Top and bottom views are also positioned so that their closest baseline is the specified distance away from the stack view’s edge. This property is only used by vertical stack views.

Stack view screenshot

UISV-spacing:
 Subview1.firstBaseline == Subview0.lastBaseline + 20
 Subview2.firstBaseline == Subview1.lastBaseline + 20

UISV-alignment:
 Subview0.leading == Subview1.leading
 Subview0.leading == Subview2.leading
 Subview0.trailing == Subview1.trailing
 Subview0.trailing == Subview2.trailing

UISV-canvas-connection:
 StackView.top == Subview0.top
 Subview2.bottom == StackView.bottom
 StackView.leading == Subview0.leading
 Subview0.trailing == StackView.trailing

The stack modifies UISV-spacing constraints by using .firstBaseline and .lastBaseline layout attributes.

Single Subview, Alignment.Center #

Stack view screenshot

UISV-canvas-connection:
 StackView.leading == Subview0.leading
 H:[Subview0]-(0)-|
 StackView.top <= Subview0.top
 V:[Subview0]-(>=0)-|
 StackView.centerY == Subview0.centerY

UISV-canvas-fit:
 StackView.height == 0 priority:49

UISV-ambiguity-suppression:
 Subview0.height == 0 priority:25

Compared to center alignment with multiple subviews, this configuration optimizes constraints by removing auxiliary spacer (_UILayoutSpacer) which is not necessary when there is just a single arranged view.

Implementation #

If you’d like to check out the actual code please take a look at Arranged sources. The entire implementation takes about 500 lines of code!

Performance #

UIStackView has a bunch of optimizations under the hood. One thing that it does for certain is updating only the constraints that need to be changed when some of the stack view options change (like, say, distribution). While this feature might be welcomed when the layout changes dynamically, I decided to avoid those optimizations to keep code as simple and reliable as possible. Even UIStackView itself has a number of scenarios in which it fails to update all of the constraints when configuration changes.

Testing #

Testing Arranged was relatively simple. All I had to do was to test that Arranged.StackView creates a set of constraints equivalent to the reference (UIStackView) in all of those cases.