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
}
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?
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 ๐ฏ