Snippet of the Week: Pinning Views


Sometimes you just need to create AutoLayout constraints in code. Maybe you can’t (or won’t) use Interface Builder. Or you still have to support iOS 8, which means UIStackView is out (though you can use OAStackView if you want). But, handling the NSLayoutConstraint API is pretty cumbersome, especially for seemingly ‘simple’ layouts.

For example, consider a view which also needs background. An easy method for this would be to create an UIImageView and make sure that its frame is always the same as the bounds of its superview.

This takes at least 4 constraints, which you then have to add to the superview;

addConstraints(NSLayoutConstraint(item: subview, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0))
addConstraints(NSLayoutConstraint(item: subview, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0.0))
addConstraints(NSLayoutConstraint(item: subview, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0))
addConstraints(NSLayoutConstraint(item: subview, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0.0))

Of course, you can cheat a little and use the Visual Format Language;

let views = [ "image" : imageView ]
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[image]-0-[text]-0-|", metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[image]-0-[text]-0-|", metrics: nil, views: views))

But this is still three lines of code, and debugging Visual Format constraints is not easy. And this is still one of the easier cases!

Can we make this simpler? Of course we can; just look at the amount of CocoaPods dealing with AutoLayout! However, I opted to create some convenience classes on my own. Below are a couple of handy convenience methods, which allow us to quickly construct some common layouts in a couple of lines.

//: NSLayoutConstraint convenience methods
public extension NSLayoutConstraint {
    
    public static func pinning(attribute: NSLayoutAttribute, ofView firstView: UIView, toView secondView: UIView, multiplier: CGFloat = 1, offset: CGFloat = 0) -> NSLayoutConstraint {
        return NSLayoutConstraint(item: firstView, attribute: attribute, relatedBy: .equal, toItem: secondView, attribute: attribute, multiplier: multiplier, constant: offset)
    }
    
    public static func pinning(attributes: [NSLayoutAttribute], ofView firstView: UIView, toView secondView: UIView, multiplier: CGFloat = 1, offset: CGFloat = 0) -> [NSLayoutConstraint] {
        return attributes.map { return NSLayoutConstraint(item: firstView, attribute: $0, relatedBy: .equal, toItem: secondView, attribute: $0, multiplier: multiplier, constant: offset) }
    }
    
    public static func pinningCenterOfView(_ firstView: UIView, toView secondView: UIView, offset: (CGPoint) = .zero) -> [NSLayoutConstraint] {
        let xConstraint = pinning(attribute: .centerX, ofView: firstView, toView: secondView, offset: offset.x)
        let yConstraint = pinning(attribute: .centerY, ofView: firstView, toView: secondView, offset: offset.y)
        return [xConstraint, yConstraint]
    }
    
    public static func pinningAttribute(attribute: NSLayoutAttribute, ofView view: UIView, toConstant constant: CGFloat) -> NSLayoutConstraint {
        return NSLayoutConstraint(item: view, attribute: attribute, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: constant)
    }
    
    public static func pinningSizeOfView(view: UIView, to size:CGSize) -> [NSLayoutConstraint] {
        let widthConstraint = NSLayoutConstraint.pinningAttribute(attribute: .width, ofView: view, toConstant: size.width)
        let heightConstraint = NSLayoutConstraint.pinningAttribute(attribute: .height, ofView: view, toConstant: size.height)
        return [widthConstraint, heightConstraint]
    }
    
}

//: UIView convenience methods
extension UIView {
    @discardableResult public func pinSubviewToCenter(_ subview: UIView, offset: CGPoint = .zero) -> (centerX: NSLayoutConstraint, centerY: NSLayoutConstraint)? {
        guard subview.superview == self else { return nil }
        let constraints = NSLayoutConstraint.pinningCenterOfView(subview, toView: self)
        self.addConstraints(constraints)
        return (centerX: constraints[0], centerY: constraints[1])
    }
    
    @discardableResult public func pinSize(_ size: CGSize) -> (width: NSLayoutConstraint, height: NSLayoutConstraint) {
        let constraints = NSLayoutConstraint.pinningSizeOfView(view: self, to: size)
        self.addConstraints(constraints)
        return (width: constraints[0], height: constraints[1])
    }
    
    @discardableResult public func pinSubviewToBounds(_ subview: UIView, offset: CGFloat = 0) -> (top: NSLayoutConstraint, left: NSLayoutConstraint, bottom: NSLayoutConstraint, right: NSLayoutConstraint)? {
        return pinSubviewToBounds(subview, ofView: self, offset: offset)
    }
    
    @discardableResult public func pinSubviewToBounds(_ subview: UIView, ofView view: UIView, offset: CGFloat = 0) -> (top: NSLayoutConstraint, left: NSLayoutConstraint, bottom: NSLayoutConstraint, right: NSLayoutConstraint)? {
        guard subview.superview == self, (view == self || view.superview == self) else { return nil }
        
        let constraints = NSLayoutConstraint.pinning(attributes: [.top, .left, .bottom, .right], ofView: subview, toView: view)
        self.addConstraints(constraints)
        return (top: constraints[0], left: constraints[1], bottom: constraints[2], right: constraints[3])
    }
    
    @discardableResult public func pinAttribute(_ subviewAttribute: NSLayoutAttribute, ofSubview subview: UIView, toAttribute attribute: NSLayoutAttribute, ofView view: UIView, multiplier: CGFloat = 1, offset: CGFloat = 0) -> NSLayoutConstraint? {
        guard subview.superview == self, (view == self || view.superview == self) else { return nil }
        
        let constraint = NSLayoutConstraint(item: subview, attribute: subviewAttribute, relatedBy: .equal, toItem: view, attribute: attribute, multiplier: multiplier, constant: offset)
        self.addConstraint(constraint)
        return constraint
    }
}

This allows us to do a couple of tasks very quickly;

// Set the size of viewA to 50x100
viewA.pinSize(CGSize(width: 50, height: 100))

// Set the center of viewB to be the same as that of its superview.
self.pinSubviewToCenter(viewB)

// Set viewC's frame to always be the same as its superview's bounds.
self.pinSubviewToBounds(viewC)

// Pin any attribute of viewD to any attribute of viewE.
self.pinAttribute(.leading, ofSubview: viewD, toAttribute: .trailing, ofView: viewE)

There, that will make our lives way easier! Of course, there are other convenience methods that fit here as well, but we’ll come up with those as needed.

Some interesting tidbits;

  • @discardableResult allows you to ignore any values returned by the method safely. This is useful here, because sometimes we might want to keep a record of the constraints (for example, to animate them.) However, usually we just ignore them, and the compiler won’t complain if we do.
  • guard <some condition> else { } – I didn’t know you could guard without defining variables, but this makes simple conditionals look a lot nicer.
  • UIView().pinX vs. NSLayoutConstraint.pinningX; some Swifty naming here. We pin subviews inside a view, but we want a constraint pinning some views.

If you think the snippet is useful – or have an improvement – send me tweet or a toot!