Search code examples
iosswiftconstraintscartography

Cartography constraints with TopLayoutGuide


I have a view which I want to look like this:

|---------------|
|               | <- navBar
|---------------|
|               | <- topView
|---------------|
|               |
|               |
|               |
|---------------|

Everything I want is to stick topView.top to navBar.bottom. I've decided to go with Cartography and implemented following code (trying to stick to MVC ofc):

In my UIViewController subclass:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    aView?.topLayoutGuide = self.topLayoutGuide // where aView is my subclass of UIView, inserted in loadView method
}

In my UIView subclass:

var topLayoutGuide: UILayoutSupport?

override func updateConstraints() {
    var constraint = NSLayoutConstraint?()
    layout(topView) { (topView) in
        constraint = topView.top == topView.superview!.top
    }
    topView.superview!.removeConstraint(constraint!)

    layout(topView) { topView in
        topView.top == topView.superview!.top + (self.topLayoutGuide?.length ?? 0)
        topView.height == 67
        topView.left == topView.superview!.left
        topView.width == topView.superview!.width
    }

    super.updateConstraints()
}

The problem is that I receive following logs and I have no idea how to fix it:

Unable to simultaneously satisfy constraints.
[...]
(
    "<NSLayoutConstraint:0x7fe6a64e4800 V:|-(64)-[UIView:0x7fe6a6505d80]   (Names: '|':MyApp.MyView:0x7fe6a360d4c0 )>",
    "<NSLayoutConstraint:0x7fe6a3538a80 V:|-(0)-[UIView:0x7fe6a6505d80]   (Names: '|':MyApp.MyView:0x7fe6a360d4c0 )>"
)

Seems I need some help. How to do it properly? I don't want to implement constraints in UIViewController and I don't want to use Storyboards.

Thanks for any help!


Solution

  • One solution: create a reference to that particular NSLayoutConstraint, and update its constant:

    var topLayoutConstraint: NSLayoutConstraint!
    
    override func updateConstraints() { 
        layout(topView) { topView in
            topLayoutConstraint = topView.top == topView.superview!.top
            topView.height == 67
            topView.left == topView.superview!.left
            topView.width == topView.superview!.width
        }
    
        super.updateConstraints()
    }
    

    ... then later, you can adjust the constant of that NSLayoutConstraint like so:

    topLayoutConstraint.constant = 50
    

    The only question that's left is - when do you update the topLayoutConstraint? One solution is to create a protocol, MyViewInterface:

    protocol MyViewInterface: class {
        var topLayoutGuideLength: CGFloat { get set }
    }
    

    Then, in our UIView, when that topLayoutGuideLength property is changed, we adjust the constant of our NSLayoutConstraint:

    public class MyView: UIView, MyViewInterface {
        public var topLayoutGuideLength: CGFloat = 0 {
            didSet {
                topLayoutConstraint.constant = topLayoutGuideLength
            }
        }
    }
    

    Finally, in our UIViewController (where our topLayoutGuide lives):

    public class myViewController: UIViewController {
    
        private var myView: MyView
    
        // ...
    
        public override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
    
            myView.topLayoutGuideLength = topLayoutGuide.length
        }
    }
    

    The end result? Whenever viewDidLayoutSubviews() is triggered on our UIViewController, our myView's topLayoutGuideLength property is updated, which triggers a change in that topLayoutConstraint's constant.