Translating constraints in a visual format to NSLayoutAnchor APIs

572 Views Asked by At

I have the following extension that I got from this SO question a few months ago, and I'd like to translate this to NSLayoutAnchor (or the more verbose layout APIs) due to the simple reason that the visual format does not support specifying the safe area layout guide.

The extension in the link is this one:

extension UIView {

    /// Adds constraints to this `UIView` instances `superview` object to make sure this always has the same size as the superview.
    /// Please note that this has no effect if its `superview` is `nil` – add this `UIView` instance as a subview before calling this.
    func bindFrameToSuperviewBounds() {
        guard let superview = self.superview else {
            print("Error! `superview` was nil – call `addSubview(view: UIView)` before calling `bindFrameToSuperviewBounds()` to fix this.")
            return
        }

        self.translatesAutoresizingMaskIntoConstraints = false
        superview.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["subview": self]))
        superview.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["subview": self]))
    }
}

My view hierarchy is:

UINavigationController -> root view controller: UIPageViewController -> First page: UIViewController -> UIScrollView -> UIImageView

When I call bindFrameToSuperviewBounds(), the result is the following:

enter image description here

Which looks mostly fine. The problem is that the image view is under the navigation bar, which I do not want. So in order to fix this, I attempted to rewrite bindFrameToSuperviewBounds() as this:

guard let superview = self.superview else {
            fatalError("Attempting to bound to a non-existing superview.")
        }

translatesAutoresizingMaskIntoConstraints = false
self.topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor).isActive = true
self.leadingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.leadingAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.trailingAnchor).isActive = true

The result is then this:

enter image description here

This time it correctly doesn't go under the navigation bar, but well... You can see it gets stretched, which I do not want.

What's the right way to translate that visual layout API to something that can take the safe area layout guide into account?

For the sake of completion, the code that sets up the scroll view and image view is below:

override func viewDidLoad() {
    super.viewDidLoad()
    imageScrollView.addSubview(imageView)
    imageScrollView.layoutIfNeeded()
    imageView.bindFrameToSuperviewBounds()
    view.addSubview(imageScrollView)
    imageView.layoutIfNeeded()
    imageScrollView.backgroundColor = .white
    imageScrollView.bindFrameToSuperviewBounds()
    imageScrollView.delegate = self
}
2

There are 2 best solutions below

0
Andy Ibanez On BEST ANSWER

With the code provided by Fattie, I was able to arrive to this solution:

guard let superview = self.superview else {
    fatalError("Attempting to bound to a non-existing superview.")
}

translatesAutoresizingMaskIntoConstraints = false
self.topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor).isActive = true
self.leadingAnchor.constraint(equalTo: superview.leadingAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: superview.trailingAnchor).isActive = true

(Now that I am only anchoring the the top to the safe layout are guide. Whether this gives problems in an iPhone X is yet to be seen).

Which gives the result I expect:

enter image description here

4
Fattie On

I honestly would not bother with the bizarre "visual" layout things, they will be gone soon.

Does this help?

extension UIView {

    // put this view "inside" it's superview, simply stretched all four ways
    // amazingly useful

    func bindEdgesToSuperview() {

        guard let s = superview else {
            preconditionFailure("`superview` nil in bindEdgesToSuperview")
        }

        translatesAutoresizingMaskIntoConstraints = false
        leadingAnchor.constraint(equalTo: s.leadingAnchor).isActive = true
        trailingAnchor.constraint(equalTo: s.trailingAnchor).isActive = true
        topAnchor.constraint(equalTo: s.topAnchor).isActive = true
        bottomAnchor.constraint(equalTo: s.bottomAnchor).isActive = true
    }

}

Just use like this ..

    textOnTop = ..  UILabel or whatever
    userPhoto.addSubview(textOnTop)
    textOnTop.bindEdgesToSuperview()

easy !

(PS - don't forget, if it's an image: You almost certainly want to set as AspectFit, at all times in all cases.)

The handles for safe layout guide are like ...

.safeAreaLayoutGuide