iosuikitswift5nslayoutconstraintnslayoutanchor

How do you, or can you, create and/or "adjust" an NSLayoutAnchor property?


Say you have some large complex custom view,

lazy var pinkThing: ..
lazy var greenThing: ..

And you want to

///For upstream views to use as they wish
var pinkCenterY: NSLayoutYAxisAnchor {
    return pinkThing.centerYAnchor
}

which is fine. You could also (pointlessly)

///For programmers who prefer to type .example than .topAnchor
var example: NSLayoutYAxisAnchor {
    return topAnchor
}

Say you want to return an anchor, which is, 42 pixels from the top of this view. So it's something like ..

///For upstream views to know where the gauges area is at
var dialsSafeArea: NSLayoutYAxisAnchor {
    return something something 42 something
}

also good to know would be how to "modify" anchors, so,

///For upstream views to know where the gauges area is
var dialsSafeArea: NSLayoutYAxisAnchor {
    return topAnchor something something 3 something
    or say
    return pinkThing.topAnchor something something 3 something
}

I may be missing something obvious but I just have not been able to figure this out.

(A workaround is that you can pathetically make an invisible view, 42 down, and return that view's anchor!)

How to? Is it possible?


Solution

  • There are actually several ways to do this, depending on what exactly you're trying to do.


    Let's say that the only way anything is ever going to be pinned to the top of a view is actually 42 pixels down from the top of the view. Then you can override that view's alignment rect insets:

    override var alignmentRectInsets: UIEdgeInsets {
        .init(top: 42, left: 0, bottom: 0, right: 0)
    }
    

    The result is that we have effectively moved the position of this view's topAnchor, and anything that pins to this view's topAnchor will be measuring from a point 42 points down from the top of the physical view.

    The nice thing about this approach is that it is automatic. No other views need to know about it; you just pin to the view's topAnchor and the right thing happens. This is very useful, for instance, when a view contains an inset drawing and what you want to do is pin to the edge of the drawing rather than the view itself.


    Another approach is to set the top of your view's layout margins. Every view has margins, but they are very little used these days; this could be a good use of one. Simply say

    myView.layoutMargins.top = 42
    

    Now when you want to pin to that thing-that-is-42-points-down from the top of myView, pin to myView.layoutMarginsGuide.topAnchor.

    (I suspect that this is in fact the approach you are really looking for.)


    Still another approach, which is a bit more work, is for the view to vend a custom layout guide. (I say "custom" because the layoutMarginsGuide is already a built-in layout guide.) Call addLayoutGuide on the view to give it a layout guide, give the layout guide an identifier so you can find it again later, and then use constraints between the layout guide and the owning view to position the layout guide:

    let guide = UILayoutGuide()
    guide.identifier = "myCoolGuide"
    myView.addLayoutGuide(guide)
    guide.topAnchor.constraint(equalTo: myView.topAnchor, constant: 42).isActive = true
    guide.leadingAnchor.constraint(equalTo: myView.leadingAnchor).isActive = true
    // and so too for trailing and bottom
    

    Then another view can be pinned to the top anchor of myView's layout guide rather than to the top anchor of myView itself. Writing code to get a reference to the layout guide is left as an exercise for the reader.

    Note that this is not the same as the "pathetic" invisible view mentioned in your question, because a UILayoutGuide is not a view. It's just a kind of invisible dimensional "frame". Indeed, UILayoutGuide was invented (by Apple) exactly so that you could do this kind of thing without the added overhead of using invisible "spacer" views.