PinLayout
PinLayout copied to clipboard
Improve Views Concatenation Methods
Description
This PR aims to improve to improve the behaviour of methods like below(of ...
or above(of ...
allowing to pin the view to the top/bottom of its superview.
Motivation and Context
Using PinLayout
frequently to chain views with the following APIs below(of ...
, above(of ...
, or after(of ...
combined with visible( ... )
method, it's easy and fast but we often find ourself facing the same problem: the view on top must always be visible. In some cases, however, it would be useful and convenient to overcome this trivial limitation by anchoring the first visible view to the superview’s edge.
The solution adopted by me and also my colleagues is to use an if else
statement inside the layout
method that check the visibility of the previous/next relative view:
func performLayout() {
firstView.pin
.top()
.size(100)
.hCenter()
.marginTop(10)
if firstView.isHidden {
secondView.pin
.top()
} else {
secondView.pin
.below(of: firstView)
}
...
}
With this pull request I would like to discuss a possible solution implemented as a set of methods that after checking the visibility state of his relative views, instead of causing the layout to fail, pin the view to the edge of the superview (top/bottom/left/right depending on the method used).
/// Example of a view that is attached below of the last visible `relativeViews`
/// or to `top()` if all relative views are hidden.
@discardableResult
public func topIfNotBelow(of relativeViews: [PinView], aligned: HorizontalAlign = .none) -> PinLayout {
func context() -> String { return relativeContext("below", relativeViews, aligned) }
return topIfNotBelow(of: relativeViews, aligned: aligned, context: context)
}
fileprivate func topIfNotBelow(of relativeViews: [PinView], aligned: HorizontalAlign, context: Context ) -> PinLayout {
guard layoutSuperview(context) != nil else { return self }
guard relativeViews.count > 0 else {
setTop(0, context)
return self
}
let anchors: [Anchor]
switch aligned {
case .left: anchors = relativeViews.map({ $0.anchor.bottomLeft })
case .center: anchors = relativeViews.map({ $0.anchor.bottomCenter })
case .right: anchors = relativeViews.map({ $0.anchor.bottomRight })
case .start: anchors = isLTR() ? relativeViews.map({ $0.anchor.bottomLeft }) : relativeViews.map({ $0.anchor.bottomRight })
case .end: anchors = isLTR() ? relativeViews.map({ $0.anchor.bottomRight }) : relativeViews.map({ $0.anchor.bottomLeft })
case .none: anchors = relativeViews.map({ $0.anchor.bottomLeft })
}
if let coordinates = computeCoordinates(forAnchors: anchors, context) {
setTop(getBottomMostCoordinate(list: coordinates), context)
applyHorizontalAlignment(aligned, coordinates: coordinates, context: context)
}
return self
}
Using this method as follow:
func performLayout() {
redView.pin
.top()
.size(100)
.hCenter()
.marginTop(10)
blueView.pin
.topIfNotBelow(of: visible([redView]))
.size(100)
.hCenter()
.marginTop(12)
greenView.pin
.topIfNotBelow(of: visible([redView, blueView]))
.size(100)
.hCenter()
.marginTop(12)
...
}
I’ll leave here a short video to show animations as well.
https://user-images.githubusercontent.com/16403933/196270311-1e2a0587-b916-4f21-9967-539953ae2c00.mp4
More Thoughts
It could also be thought of a version of this proposal that improves the names of the methods added by me.
Existing functions (such as below(of ...)
) could be improved by adding a parameter that describes where the view should be pin in case there are no relative views visible (as default it will maintain the default behaviour):
enum ParentVerticaAnchor {
case none
case top
case bottom
case center
}
fileprivate func below(of relativeViews: [PinView], aligned: HorizontalAlign, ifNeeded anchor: ParentVerticaAnchor = .none, context: Context) -> PinLayout {
guard layoutSuperview(context) != nil else { return self }
switch anchor {
case .none:
guard relativeViews.count > 0 else {
warnWontBeApplied("At least one view must be visible (i.e. UIView.isHidden != true) ", context)
return self
}
case .top:
guard relativeViews.count > 0 else {
setTop(0, context)
return self
}
case .bottom:
guard relativeViews.count > 0 else {
setBottom(0, context)
return self
}
case .center:
guard relativeViews.count > 0 else {
setVerticalCenter(0, context)
return self
}
}
let anchors: [Anchor]
switch aligned {
case .left: anchors = relativeViews.map({ $0.anchor.bottomLeft })
case .center: anchors = relativeViews.map({ $0.anchor.bottomCenter })
case .right: anchors = relativeViews.map({ $0.anchor.bottomRight })
case .start: anchors = isLTR() ? relativeViews.map({ $0.anchor.bottomLeft }) : relativeViews.map({ $0.anchor.bottomRight })
case .end: anchors = isLTR() ? relativeViews.map({ $0.anchor.bottomRight }) : relativeViews.map({ $0.anchor.bottomLeft })
case .none: anchors = relativeViews.map({ $0.anchor.bottomLeft })
}
if let coordinates = computeCoordinates(forAnchors: anchors, context) {
setTop(getBottomMostCoordinate(list: coordinates), context)
applyHorizontalAlignment(aligned, coordinates: coordinates, context: context)
}
return self
}