July 1, 2019

Experimenting with NSLayoutMargin

2 minutes read



Requirement

So, we have a custom view, where we need to position multiple subviews. First thing that comes to mind is - use UIStackView!

But wait.. UIStackView is a subclass of UIView, thus, it will also get rendered. And if we nest stack-views within stack-views within stack-views… (which is super easy way to position stuff), and if we use it in tableview, it could impact scrolling fps, if the UI is complicated enough! And don’t forget about increased load time. (Ok, I might be overreacting, but still)

Solution

UILayoutMargin helps us with that. It’s basically just like a view, that you can position, but it does not get rendered, nor it nests stuff. It just gives margins to easier position elements.

1.) Opposite sides

This is how easy it is to position two elements on opposite sides.

(black frame is just to mark guide frame)

// Create box 1
let box1 = UIView()
box1.translatesAutoresizingMaskIntoConstraints = false
box1.backgroundColor = .red
self.view.addSubview(box1)

// Create box 2
let box2 = UIView()
box2.translatesAutoresizingMaskIntoConstraints = false
box2.backgroundColor = .blue
self.view.addSubview(box2)
        
// Create UILayoutGuide
let guide = UILayoutGuide()
self.view.addLayoutGuide(guide)

// Position guide to be 50 pix from left and right
guide.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 50).isActive = true
guide.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -50).isActive = true

// Set boxes width/height to 50
box1.widthAnchor.constraint(equalToConstant: 50).isActive = true
box1.heightAnchor.constraint(equalToConstant: 50).isActive = true
box2.widthAnchor.constraint(equalToConstant: 50).isActive = true
box2.heightAnchor.constraint(equalToConstant: 50).isActive = true

// Position boxes in y-center
box1.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true
box2.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true

// And position boxes
box1.leftAnchor.constraint(equalTo: guide.leftAnchor).isActive = true
box2.rightAnchor.constraint(equalTo: guide.rightAnchor).isActive = true


2.) Spacers

In case we need to center both objects, within a layout guide, we can use multiple layout guides, as spacers.

(black frame is just to mark guide frame)

// Create box 1
let box1 = UIView()
box1.translatesAutoresizingMaskIntoConstraints = false
box1.backgroundColor = .red
self.view.addSubview(box1)

// Create box 2
let box2 = UIView()
box2.translatesAutoresizingMaskIntoConstraints = false
box2.backgroundColor = .blue
self.view.addSubview(box2)
        
// Create UILayoutGuide
let guide = UILayoutGuide()
self.view.addLayoutGuide(guide)

// Create spacer UILayoutGuides
let space1 = UILayoutGuide()
self.view.addLayoutGuide(space1)
let space2 = UILayoutGuide()
self.view.addLayoutGuide(space2)
let space3 = UILayoutGuide()
self.view.addLayoutGuide(space3)

// Position guide to be 50 pix from left and right
guide.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 50).isActive = true
guide.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -50).isActive = true

// Provide width constraints for spaces, but set them as low priority, so
// that they would strech instead of boxes (thus, width value is not important).
let space1Constraint = space1.widthAnchor.constraint(equalToConstant: 1)
space1Constraint.priority = .defaultLow
space1Constraint.isActive = true
let space2Constraint = space2.widthAnchor.constraint(equalTo: space1.widthAnchor)
space2Constraint.priority = .defaultLow
space2Constraint.isActive = true
let space3Constraint = space3.widthAnchor.constraint(equalTo: space1.widthAnchor)
space3Constraint.priority = .defaultLow
space3Constraint.isActive = true

// Set boxes width/height to 50
box1.widthAnchor.constraint(equalToConstant: 50).isActive = true
box1.heightAnchor.constraint(equalToConstant: 50).isActive = true
box2.widthAnchor.constraint(equalToConstant: 50).isActive = true
box2.heightAnchor.constraint(equalToConstant: 50).isActive = true

// Position boxes in y-center
box1.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true
box2.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true

// Position boxes with spacers. Notice that we don't provide space2 anchors,
// because it's anchors are set by box1 and box2.
space1.leftAnchor.constraint(equalTo: guide.leftAnchor).isActive = true
box1.leftAnchor.constraint(equalTo: space1.rightAnchor).isActive = true
box1.rightAnchor.constraint(equalTo: space2.leftAnchor).isActive = true
box2.leftAnchor.constraint(equalTo: space2.rightAnchor).isActive = true
box2.rightAnchor.constraint(equalTo: space3.leftAnchor).isActive = true
space3.rightAnchor.constraint(equalTo: guide.rightAnchor).isActive = true


3.) Different size spacers

What if we want different size spacers? No problem! Simply provide multiplier, when setting spacer width!

(black frame is just to mark guide frame)

// Create box 1
let box1 = UIView()
box1.translatesAutoresizingMaskIntoConstraints = false
box1.backgroundColor = .red
self.view.addSubview(box1)

// Create box 2
let box2 = UIView()
box2.translatesAutoresizingMaskIntoConstraints = false
box2.backgroundColor = .blue
self.view.addSubview(box2)
        
// Create UILayoutGuide
let guide = UILayoutGuide()
self.view.addLayoutGuide(guide)

// Create spacer UILayoutGuides
let space1 = UILayoutGuide()
self.view.addLayoutGuide(space1)
let space2 = UILayoutGuide()
self.view.addLayoutGuide(space2)
let space3 = UILayoutGuide()
self.view.addLayoutGuide(space3)

// Position guide to be 50 pix from left and right
guide.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 50).isActive = true
guide.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -50).isActive = true

// Provide width constraints for spaces, but set them as low priority, so
// that they would strech instead of boxes (thus, width value is not important).
// Space 2 width multiplier is 3 - it will be 3x larger than other spaces.
let space1Constraint = space1.widthAnchor.constraint(equalToConstant: 1)
space1Constraint.priority = .defaultLow
space1Constraint.isActive = true
let space2Constraint = space2.widthAnchor.constraint(equalTo: space1.widthAnchor, multiplier: 3)
space2Constraint.priority = .defaultLow
space2Constraint.isActive = true
let space3Constraint = space3.widthAnchor.constraint(equalTo: space1.widthAnchor)
space3Constraint.priority = .defaultLow
space3Constraint.isActive = true

// Set boxes width/height to 50
box1.widthAnchor.constraint(equalToConstant: 50).isActive = true
box1.heightAnchor.constraint(equalToConstant: 50).isActive = true
box2.widthAnchor.constraint(equalToConstant: 50).isActive = true
box2.heightAnchor.constraint(equalToConstant: 50).isActive = true

// Position boxes in y-center
box1.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true
box2.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true

// Position boxes with spacers. Notice that we don't provide space2 anchors,
// because it's anchors are set by box1 and box2.
space1.leftAnchor.constraint(equalTo: guide.leftAnchor).isActive = true
box1.leftAnchor.constraint(equalTo: space1.rightAnchor).isActive = true
box1.rightAnchor.constraint(equalTo: space2.leftAnchor).isActive = true
box2.leftAnchor.constraint(equalTo: space2.rightAnchor).isActive = true
box2.rightAnchor.constraint(equalTo: space3.leftAnchor).isActive = true
space3.rightAnchor.constraint(equalTo: guide.rightAnchor).isActive = true


4.) Both boxes relatively centered

What if we have 2 objects that are different width, but center them both (so that on left and right side it would have same space) ?

(black frame is just to mark guide frame)

// Create box 1
let box1 = UIView()
box1.translatesAutoresizingMaskIntoConstraints = false
box1.backgroundColor = .red
self.view.addSubview(box1)

// Create box 2
let box2 = UIView()
box2.translatesAutoresizingMaskIntoConstraints = false
box2.backgroundColor = .blue
self.view.addSubview(box2)
        
// Create UILayoutGuide
let guide = UILayoutGuide()
self.view.addLayoutGuide(guide)

// Create spacer UILayoutGuides
let space1 = UILayoutGuide()
self.view.addLayoutGuide(space1)
let space2 = UILayoutGuide()
self.view.addLayoutGuide(space2)
let space3 = UILayoutGuide()
self.view.addLayoutGuide(space3)

// Position guide to be 50 pix from left and right
guide.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 50).isActive = true
guide.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -50).isActive = true

// Provide width constraints for spaces, but set them as low priority, so
// that they would strech instead of boxes (thus, width value is not important).
// Space 2 width multiplier is 0.5 - it will be 2x smaller than other spaces.
let space1Constraint = space1.widthAnchor.constraint(equalToConstant: 1)
space1Constraint.priority = .defaultLow
space1Constraint.isActive = true
let space2Constraint = space2.widthAnchor.constraint(equalTo: space1.widthAnchor, multiplier: 0.5)
space2Constraint.priority = .defaultLow
space2Constraint.isActive = true
let space3Constraint = space3.widthAnchor.constraint(equalTo: space1.widthAnchor)
space3Constraint.priority = .defaultLow
space3Constraint.isActive = true

// Set boxes width/height to 50, but first box will be 120 wide
box1.widthAnchor.constraint(equalToConstant: 120).isActive = true
box1.heightAnchor.constraint(equalToConstant: 50).isActive = true
box2.widthAnchor.constraint(equalToConstant: 50).isActive = true
box2.heightAnchor.constraint(equalToConstant: 50).isActive = true

// Position boxes in y-center
box1.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true
box2.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true

// Position boxes with spacers. Notice that we don't provide space2 anchors,
// because it's anchors are set by box1 and box2.
space1.leftAnchor.constraint(equalTo: guide.leftAnchor).isActive = true
box1.leftAnchor.constraint(equalTo: space1.rightAnchor).isActive = true
box1.rightAnchor.constraint(equalTo: space2.leftAnchor).isActive = true
box2.leftAnchor.constraint(equalTo: space2.rightAnchor).isActive = true
box2.rightAnchor.constraint(equalTo: space3.leftAnchor).isActive = true
space3.rightAnchor.constraint(equalTo: guide.rightAnchor).isActive = true


P.S.

I didn’t provide, but I also y-centered layout guides:

 guide.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
 space1.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true

To make UILayoutGuide frame visible, I simply added a view with border. Use it for debugging only.

DispatchQueue.main.asyncAfter(deadline: .now()) {
    for guide in self.view.layoutGuides {
        let view1 = UIView(frame: guide.layoutFrame)
        view1.layer.borderColor = UIColor.black.cgColor
        view1.layer.borderWidth = 1.0
        self.view.addSubview(view1)    
    }
}

But in order to see those frames, you then also need to provide a height for layout guides:

guide.heightAnchor.constraint(equalToConstant: 100).isActive = true
space1.heightAnchor.constraint(equalToConstant: 20).isActive = true

Playground file with the experiments