Experimenting with NSLayoutMargin
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.
// 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.
// 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!
// 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) ?
// 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