kean/Align

Extensions, Spacing, IsInverted and Others ๐Ÿ™‚

wow-such-amazing opened this issue ยท 4 comments

Hi!

Thanks for building a small and simple Auto Layout library. I've been integrating it into our project and came up with couple thoughts/questions about the implementation ๐Ÿ™‚

Thanks in advance for having a look at them!

AddSubview

I've found this extension in the tests part of the project and I think it's very useful. What was the motivation to remove it from the project itself?

public extension UIView {
    @discardableResult @nonobjc func addSubview(_ a: UIView, constraints: (LayoutAnchors<UIView>) -> Void) -> Constraints {
        addSubview(a)
        return Constraints(for: a, constraints)
    }
}

LayoutAnchors

I've been able to easily pin anchors to the superview, but for the SafeArea I didn't find a simple way to do that. So I've implemented this extension. Though I've been wondering if it fits the Align philosophy. Maybe I don't see some big picture here ๐Ÿค”

public extension LayoutAnchors where Base: UIView {
    var superviewSafeArea: LayoutItem? {
        return self.base.superview?.safeAreaLayoutGuide
    }

    @discardableResult func toSafeArea(insets: CGFloat = 0.0) -> [NSLayoutConstraint] {
        edges.pin(to: self.base.superview?.safeAreaLayoutGuide, insets: insets)
    }
}

Combining AddSubview + LayoutAnchors

Combining the above 2 I've been able to write code like this:

view.addSubview(loadingView) {
    $0.toSafeArea()
}

Or this:

view.addSubview(progressView) {
    $0.top.pin(to: $0.superviewSafeArea)
    $0.leading.pin()
    $0.trailing.pin()
}

So from my perspective the UIView+AddSubview is super nice addition ๐Ÿ™‚

Align/Spacing -> Pin

I wanted to have the same interface so I've created couple more extensions:

public extension AnchorCollectionCenter {
    @discardableResult func pin() -> [NSLayoutConstraint] {
        self.align()
    }

    @discardableResult func pin<Item: LayoutItem>(to item: Item) -> [NSLayoutConstraint] {
        self.align(with: item)
    }
}

public extension Anchor where Type: AnchorType.Center {
    @discardableResult func pin(offset: CGFloat = 0) -> NSLayoutConstraint {
        self.align(offset: offset)
    }
}

public extension Anchor where Type: AnchorType.Edge {
    @discardableResult func pin(to anchor: Anchor<Type, Axis>, spacing: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
        self.spacing(spacing, to: anchor, relation: relation)
    }
}

Is there some specific reasons why you kept spacing and align and didn't rename them to pin?

IsInverted

I've been looking at the source code and found that the implementation of the variable isInverted is different in pin and spacing functions.
For example spacing has this check:

(attribute == .trailing && anchor.attribute == .leading)

But doesn't have this one:

(attribute == .trailing && anchor.attribute == .trailing)

Which in my opinion is also needed to make sure we invert the spacing in this case. So maybe we can simplify it and use the same logic as in the pin?

let isInverted = [.trailing, .right, .bottom].contains(attribute)

While writing the last point about the IsInverted I've noticed that spacing also inverts the relation. Which is probably the reason why isInverted is calculated differently here.

But still the spacing function allows to write the following:

view1.anchors.trailing.spacing(8, to: view2.anchors.trailing)

Which will produce wrong layout ๐Ÿค”

Also maybe properties of the Anchor type could be public so that we could access them in our own extensions? ๐ŸŒš

public struct Anchor<Type, Axis> { // type and axis are phantom types
    public let item: LayoutItem
    public let attribute: NSLayoutConstraint.Attribute
    public let offset: CGFloat
    public let multiplier: CGFloat
}
kean commented

AddSubview

UIView+AddSubview is a nice convenience API! The initial version of the framework shipped with it (and also with some convenience stack view APIs which I later improved and shipped as a separate micro-framework. But I ultimately decided to remove it because I wanted to keep the framework focused and I'm generally not a fan of extending the native APIs. You can always add it directly to your app if you want to use it.

Safe Area

It's for the same reasons I kept safe area (and margins guides) helpers out of the library. I think the existing API is reasonable clear:

view.anchors.edges.pin()
view.anchors.edges.pin(to: container.layoutMarginsGuide)
view.anchors.edges.pin(to: container.safeAreaLayoutGuide)

If you use these a lot, I think it's worth adding a few extension directly to the view:

// or whichever naming you prefer
view.pinEdgesToSuperviewEdges()
view.pinEdgesToLayoutMargins()
view.pinEdgesToSafeArea()

I prefer view.achors.edges.pin() for consistency. The idea is that with Align, you first select what anchor or collection of anchors you want to operate with, and then you specify the operation. And I don't think that having to spell out container.safeAreaLayoutGuide is too bad. On a plus side, it reduces the API surface of the framework significantly - you use the already existing native APIs.

Naming

The idea is, for better or worse, to have two separate sets of APIs. The first set is what I call "Core API". These APIs just mechanically add the constraints and don't have any additional logic or special meaning behind them. And then there are "Semantic" APIs that do.

spacing is one of these "Semantic" APIs. It automatically flips the constant depending on the attribute. This way these two calls are equivalent so you don't need to think twice whether to negate the constant or not:

a.anchors.bottom.spacing(10, to: b.anchors.top)
b.anchors.top.spacing(10, to: a.anchors.bottom)

You can add spacings in a similar way in the Interface Builder, and that's where I took the inspiration from.

The align naming is probably less important. I just wanted it to make sense from the natural language perspective. If you to make axis X equal axis X of another view, you say "align". This idea breaks down a bit when you think about edges. You could technically also "align" leading to trailing, etc.

So, I'm not sure it's worth using a separate word for axis โ€“ it makes the API slightly harder to learn. And in hindsight, spacing isn't great either because it's not a verb. I'm willing to change it if there a good alternative that has some significant advantages.

IsInverted

I designed it to be used for adding spacings between views that are adjacent to each other. So only "top' to "bottom" and "left" to "right" combinations are considered valid. For other scenarios, you can use equal constraints directly that don't have any specific meaning. And "semantic" APIs are designed for some common operations that require a bit more thinking/steps that just manually adding a constraint.

I'm not sure I understand the trailing to trailing scenario? Can you show visually what you mean and explain what's you expectation, please?

kean commented

Also maybe properties of the Anchor type could be public so that we could access them in our own extensions? ๐ŸŒš

Do you have an example where you'd like to access them?

I was going for the smallest API surface possible in the framework to make sure it's easy to learn. I found other frameworks a bit unwieldy with their massive APIs and implementations: sometimes 10x the size of Align. This is the main reason I built my own.

Thank you for all the answers, really appreciate ๐Ÿ™Œ

I'm not sure I understand the trailing to trailing scenario? Can you show visually what you mean and explain what's you expectation, please?

In my example I've found a way to setup constraints properly, but my point is that the spacing function allows to create a constraint that will be wrong actually. Cause it doesn't invert spacing or relation if you try to create a constraint between same anchors like trailing. So maybe at least an assertion or something could help here.

Do you have an example where you'd like to access them?

So I wanted to try and see if I can create a function that would solve the problem that I've described in the point above. But I couldn't access the attribute property for example. I don't think there is a high need to have these properties public. But I was thinking that even if they are public it shouldn't break the philosophy of your library, but should allow other devs to extend it more if they want ๐Ÿค”

I was going for the smallest API surface possible in the framework to make sure it's easy to learn.

That's actually why we have chosen your framework ๐Ÿ’ฏ