Fattie
Fattie

Reputation: 12272

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?

Upvotes: -2

Views: 81

Answers (2)

Fattie
Fattie

Reputation: 12272

For anyone googling here, just another example from the superb ticked answer on this page; here a single guide only needed

Vendor ...

lazy var someTopAnchor: NSLayoutYAxisAnchor = {
    let g = UILayoutGuide()
    addLayoutGuide(g)
    g.topAnchor.constraint(equalTo: topAnchor, constant: 42).isActive = true
    print("added/returned a layout anchor")
    return g.topAnchor
}()

upstream ...

lazy var red: UIView = {
    let v = UIView.Typical
    v.backgroundColor = .red
    view.addSubview(v)
    NSLayoutConstraint.activate([
        v.heightAnchor.constraint(equalToConstant: 100),
        v.widthAnchor.constraint(equalToConstant: 100),
        v.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 100),
        v.topAnchor.constraint(equalTo: mapView.someTopAnchor)
    ])
    return v
}()

real-world example use

///Use this to be above the "fine print" (Maps logo, 'legal' link)
///which UIKit adds to a map view.
lazy var finePrintSafaAreaBottom: NSLayoutYAxisAnchor = {
    let g = UILayoutGuide()
    addLayoutGuide(g)
    let bestGuessFinePrintView = {
        for v in subviews {
            if "MKAttributionLabel" .. etc etc
    }()
    let comfortable = bestGuessFinePrintView.minY - 8
    g.topAnchor.constraint(equalTo: topAnchor,
            constant: comfortable).isActive = true
    return g.topAnchor
}()

(*) Smartass tip ...

Animating the layout guides you vend can be a powerful way to encapsulate stuff.

Upvotes: 0

matt
matt

Reputation: 535890

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.

Upvotes: 1

Related Questions