“This is the second day of my participation in the November Gwen Challenge. See details of the event: The Last Gwen Challenge 2021”.
As you can see from the previous article, even though the Auto Layout API has improved dramatically over the years – especially with the introduction of Layout anchors in iOS 9 – it is still quite tedious and onerous. Today let’s do a simple encapsulation attempt:
The effect
Here’s an example of the code we wrote using the native Auto Layout API:
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.topAnchor.constraint(
equalTo: button.bottomAnchor,
constant: 20
),
label.leadingAnchor.constraint(
equalTo: button.leadingAnchor
),
label.widthAnchor.constraint(
lessThanOrEqualTo: view.widthAnchor,
constant: -40
)
])
Copy the code
After our encapsulation, it is written as follows:
label.layout {
$0.top == button.bottomAnchor + 20
$0.leading == button.leadingAnchor
$0.width <= view.widthAnchor - 40
}
Copy the code
By comparison, this is much simpler and makes it easier for us to lay out constraints. Let’s make it happen.
implementation
What we essentially want to do is wrap the default Auto Layout based NSLayoutAnchor API (iOS9) so that it still has a perfectly normal Layout constraint when called. Then all layout anchors are implemented using a generic class called NSLayoutAnchor, because different anchors behave differently depending on whether they are used for positioning or size and things like that.
Define a protocol to implement the following requirements (other similar requirements can be added, preferably with the same method name as the system) :
protocol LayoutAnchor {
func constraint(equalTo anchor: Self, constant: CGFloat) -> NSLayoutConstraint
func constraint(greaterThanOrEqualTo anchor: Self, constant: CGFloat) -> NSLayoutConstraint
func constraint(lessThanOrEqualTo anchor: Self, constant: CGFloat) -> NSLayoutConstraint
}
Copy the code
Since NSLayoutAnchor already implements the above method, all it needs to do is make it compliant with our new protocol, simply adding an empty extension:
extension NSLayoutAnchor: LayoutAnchor {}
Copy the code
Next, we need a way to refer to individual anchor points in a simpler way. To do this, we define a LayoutProperty type, which is used in DSL mode to establish constraints on top, leading, width, etc.
struct LayoutProperty<Anchor: LayoutAnchor> {
fileprivate let anchor: Anchor
}
Copy the code
The anchor property above, Fileprivate, makes it accessible only in the file where we defined the layout, preventing implementation details from being exposed externally.
Next we need to design a LayoutProxy object that acts as a proxy to define the layout for the current view. This object should contain all the layout properties, such as the properties of common anchor points such as leading,top and width:
class LayoutProxy {
lazy var leading = property(with: view.leadingAnchor)
lazy var trailing = property(with: view.trailingAnchor)
lazy var top = property(with: view.topAnchor)
lazy var bottom = property(with: view.bottomAnchor)
lazy var width = property(with: view.widthAnchor)
lazy var height = property(with: view.heightAnchor)
private let view: UIView
fileprivate init(view: UIView) {
self.view = view
}
private func property<A: LayoutAnchor>(with anchor: A) -> LayoutProperty<A> {
return LayoutProperty(anchor: anchor)
}
}
Copy the code
We wrote all of the layout attributes lazy above so that we only construct them when we need them. Especially if we keep adding support for more type anchors.
Next, use layout properties to add constraints.
extension LayoutProperty {
func equal(to otherAnchor: Anchor, offsetBy constant: CGFloat = 0) {
anchor.constraint( equalTo: otherAnchor, constant: constant).isActive = true
}
func greaterThanOrEqual(to otherAnchor: Anchor, offsetBy constant: CGFloat = 0) {
anchor.constraint(greaterThanOrEqualTo: otherAnchor, constant: constant).isActive = true
}
func lessThanOrEqual(to otherAnchor: Anchor, offsetBy constant: CGFloat = 0) {
anchor.constraint(lessThanOrEqualTo: otherAnchor, constant: constant).isActive = true
}
}
Copy the code
We can now use LayoutProxy to manually create an instance of the view that defines the layout and then call methods on its layout properties to add constraints, as shown below:
let proxy = LayoutProxy(view: label)
proxy.top.equal(to: button.bottomAnchor, offsetBy: 20)
proxy.leading.equal(to: button.leadingAnchor)
proxy.width.lessThanOrEqual(to: view.widthAnchor, offsetBy: -40)
Copy the code
This is much less verbose than the original automatic layout API! But it still doesn’t have the same effect as SnapKit. So let’s keep optimizing.
Next we add an extension to UIView that adds a method that uses a closure to call the view itself in reverse. We will also take this opportunity to automatically set translatesAutoresizingMaskIntoConstraints to false, which further makes our easy to use API, as shown below:
extension UIView {
func layout(using closure: (LayoutProxy) -> Void) {
translatesAutoresizingMaskIntoConstraints = false
closure(LayoutProxy(view: self))
}
}
Copy the code
By writing this small extension we can optimize the code as follows:
label.layout {
$0.top.equal(to: button.bottomAnchor, offsetBy: 20)
$0.leading.equal(to: button.leadingAnchor)
$0.width.lessThanOrEqual(to: view.widthAnchor, offsetBy: -40)
}
Copy the code
This should look a lot like what we optimized at the end. Basically just a few dozen lines of code to build an automatic layout library that is readable and easy to use (compared to official layout code, of course).
Next we need custom operators to improve our layout file — starting with overloading the + and – operators, which allow us to combine layout anchors and constants into a tuple — and later let’s operate on them as a unit:
func +<A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> (A, CGFloat) {
return (lhs, rhs)
}
func -<A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> (A, CGFloat) {
return (lhs, -rhs)
}
Copy the code
Next, let’s add an overload, and let’s actually define the constraint — starting with the == operator. We need two overloads, one with only anchor points on the right and the other with offset points on the right:
func ==<A: LayoutAnchor>(lhs: LayoutProperty<A>,
rhs: (A, CGFloat)) {
lhs.equal(to: rhs.0, offsetBy: rhs.1)
}
func ==<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) {
lhs.equal(to: rhs)
}
Copy the code
By using these operators you can display the syntactic sugar of the initial result display. Let’s do the same for the >= and <= operators:
func >=<A: LayoutAnchor>(lhs: LayoutProperty<A>,
rhs: (A, CGFloat)) {
lhs.greaterThanOrEqual(to: rhs.0, offsetBy: rhs.1)
}
func >=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) {
lhs.greaterThanOrEqual(to: rhs)
}
func <=<A: LayoutAnchor>(lhs: LayoutProperty<A>,
rhs: (A, CGFloat)) {
lhs.lessThanOrEqual(to: rhs.0, offsetBy: rhs.1)
}
func <=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) {
lhs.lessThanOrEqual(to: rhs)
}
Copy the code
We can now use the new operator overload to define all of our layout constraints using expressions like those shown in the initial results:
label.layout {
$0.top == button.bottomAnchor + 20
$0.leading == button.leadingAnchor
$0.width <= view.widthAnchor - 40
}
Copy the code
Compare this to Apple Auto Layout’s native rendering – the difference is huge! 😁. This article uses DSLS, custom operators, protocols and other knowledge, which will be explained in the next article.
This leaves one topic: what happens when layout constraints conflict? And why do layout problems occur?
Excellent third-party layout library:
- Masonry
- SnapKit