Custom operators 🥳
Swift lets us easily define custom operators. I am going to demonstrate how it is done using a simple use case - managing and layouting views.
💯
Simplifying code
Manipulating views in code usually involves a lot of boilerplate code. We have to think about dimensions, frames, or constraints. Apple doesn’t give us a break here, as auto-layout API offered by UIKit is not trivial and not very natural.
Luckily for us, we can make it better by using custom operators!
Consider the following code:
let parent = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) // 1
let child1 = UIView()
let child2 = UIView()
let child3 = UIView()
parent.addSubview(child1) // 2
child1.translatesAutoresizingMaskIntoConstraints = false // 3
child1.widthAnchor.constraint(equalToConstant: 50).isActive = true // 4
child1.heightAnchor.constraint(equalToConstant: 50).isActive = true
child1.topAnchor.constraint(equalTo: parent.topAnchor).isActive = true
child1.trailingAnchor.constraint(equalTo: parent.trailingAnchor).isActive = true
parent.addSubview(child2)
child2.translatesAutoresizingMaskIntoConstraints = false
child2.widthAnchor.constraint(equalToConstant: 40).isActive = true
child2.heightAnchor.constraint(equalToConstant: 30).isActive = true
child2.topAnchor.constraint(equalTo: parent.topAnchor).isActive = true
child2.leadingAnchor.constraint(equalTo: parent.leadingAnchor).isActive = true
parent.addSubview(child3)
child3.translatesAutoresizingMaskIntoConstraints = false
child3.heightAnchor.constraint(equalToConstant: 20).isActive = true
child3.topAnchor.constraint(equalTo: parent.topAnchor).isActive = true
child3.trailingAnchor.constraint(equalTo: parent.trailingAnchor).isActive = true
child3.leadingAnchor.constraint(equalTo: parent.leadingAnchor).isActive = true
Consider how much boilerplate code we have to use in order to set up UI elements programmatcially:
- We have to create views with explicit frames and dimensions.
- We have to add views to other views.
- Since we want to use UIKit auto layout constraints, we need to tell views to not deduce implicit constraints based on the initial frame.
- We have to add layout constraints using Apple’s far-from-human-readable syntax.
Imagine doing this for each screen in your app, for multiple views.
Defining custom operators
It would be nice if we could easily add subviews to each other. Let’s define a custom operator for this purpose:
infix operator +== // 1
func +==(_ parent: UIView, _ child: UIView) { // 2
parent.addSubview(child) // 3
}
- We define an infix operator, which means it goes between two operands. We choose +== as the operator symbol.
- The operator implementation is a func which takes two arguments, parent as the left-hand-side argument, and child as the right-hande-side argument.
- We add child as the parent subview.
Now we can write:
parent +== child1
parent +== child2
parent +== child3
Let’s improve this operator even more. It would be nice if we could add all subviews in a single line rather than have a separate line for every subview.
precedencegroup UIViewAddSubviewPrecedence { // 1
associativity: left
}
infix operator +==: UIViewAddSubviewPrecedence // 2
@discardableResult // 3
func +==(_ parent: UIView, _ child: UIView) -> UIView {
parent.addSubview(child)
return parent // 4
}
- We define a new precedence group, and define its associativity as left. This means that when the compiler has to evaluate multiple operators in sequence, it will go from left to right.
- We conform to our new precedence group.
- We mark the operator result as @discardableResult. This tells the compiler that the use of the operator result is not mandatory and eliminates warnings about not using it.
- We return the parent as the operator result.
Now we can write:
parent +== child1 +== child2 +== child3
Auto layout - give me some sugar
We still didn’t solve the boilerplate code involving the use of auto-layout constraints. We will achieve this in two steps. The first step is to define a few helper functions to encapsulate the boilerplate code (and be able to forget about it altogether :)).
extension UIView {
func setHeight(_ height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: height).isActive = true
}
func setWidth(_ width: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
widthAnchor.constraint(equalToConstant: width).isActive = true
}
func pinLeading(to view: UIView) {
translatesAutoresizingMaskIntoConstraints = false
leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
}
func pinTrailing(to view: UIView) {
translatesAutoresizingMaskIntoConstraints = false
trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
func pinTop(to view: UIView) {
translatesAutoresizingMaskIntoConstraints = false
topAnchor.constraint(equalTo: view.topAnchor).isActive = true
}
}
We extend UIView and add multiple functions with a human readable syntax which defines layout constraints internally.
In the next step, we will define more custom operators which will use these functions:
infix operator |<- // pin to leading edge
infix operator ->| // pin to trailing edge
infix operator ^^ // pin to top
infix operator ||| // set height
infix operator --- // set width
func |<-(_ lhs: UIView, _ rhs: UIView) {
rhs.pinLeading(to: lhs)
}
func ->|(_ lhs: UIView, _ rhs: UIView) {
lhs.pinTrailing(to: rhs)
}
func ^^(_ lhs: UIView, _ rhs: UIView) {
lhs.pinTop(to: rhs)
}
func |||(_ view: UIView, _ height: CGFloat) {
view.setHeight(height)
}
func ---(_ view: UIView, _ width: CGFloat) {
view.setWidth(width)
}
Now we can do lots of view manipulations using our brand new operators:
child1 ||| 50
child1 --- 50
child1 ->| parent
child1 ^^ parent
child2 ||| 30
child2 --- 40
child2 |<- parent
child2 ^^ parent
child3 ||| 20
child3 ^^ parent
child3 |<- parent
child3 ->| parent
Pretty cool 😎
Conclusion
We’ve learnt how we can define custom oeprators in SWift, and how we could use them for making boiler plate code much cleaner. ⭐️