Auto Layout: How to do percentage-based layouts

By uliwitness

I recently had to implement a two-directional slider (I.e. a box with an indicator that can go anywhere). I wanted to do it using modern auto layout, and I needed it to resize properly on rotation without me having to change internal variables.

That meant that the position of the slider knob would have to be specified as a percentage (well, fraction) in the multiplier of the constraints, so that whatever size the slider UIView had, it would adapt.

My first attempt was to simply specify the position as a multiple of the height. So [self.indicatorView.centerXAnchor.constrainToAnchor: self.widthAnchor multiplier: 0.0].active = YES would be left, and multiplier: 1.0 would be right (and analogously for the Y direction).

That worked fine, but had the problem that the indicator could end up “tucked under” the left or right edges. I tried using UIView‘s layoutInsets, but that didn’t work either. In the end, I would have to manually add the fraction that my indicator’s width corresponded to to the ends to avoid that from happening. Might as well use pixels, then.

The autolayout margin guides

Then I remembered I could just add additional UILayoutGuides (MacOS has NSLayoutGuides) to define the margins I wanted to keep clear, then define another layout guide relative to those for the actual draggable area, relative to which I could constrain my indicator.

So first I built 4 guides that were pinned to the edges, had a width (resp. height) of 0.5 × indicatorView.widthAnchor (resp. heightAnchor) and a height/position (resp. width/position) same as the slider view.

Now we had the margins. Then I added a 5th guide that covered the draggable area inside those guides. Then took the old constraints and made them relative to this guide instead of the entire view.

That didn’t work. The height starts at 0, so if used as a position, it would always end up in the upper left. And if I added a constant the size of the margins, I’d have something that wouldn’t update when the view resized again. Might as well use pixels, then.

Drag area and indicator position layout guide (in blue)

Then it struck me: Why not just add another guide? The guide is pinned to the upper left of the draggable area, and its width/height are percentages of the draggable area’s height. I can now set the multiplier on the width/height constraints to my slider percentages, and the lower right corner of this 6th “indicator position” guide would be exactly where I want the indicator to be.

So I just change this guide’s multipliers when the indicator moves, and bind the indicator view’s center to the bottom and right anchors of the indicator position guide, and it all works!

Note

You may note that I keep talking about changing the multiplier on constraints. Yeah, that’s not really possible, the only thing on a constraint that can change is the constant (well, and the identifier, but that would ruin the joke).

So yeah, wherever you read that, what I do is remove and recreate the constraint. Sadly, constraints do not have a -removeFromSuperview method, so what I really have to do is walk from a constraint’s firstItem and secondItem property up to their common ancestor and tell it to remove the constraint (if they are views … if they are guides, that means they’re constraints on self or one of its superviews).

Share your thoughts