In issue #41 in which we have built an app that uses a photo camera to capture one’s loayalty cards we used a pattern that we named Builder to configure properties of objects. How does the code look like when we use Builder pattern?
|
1 2 3 4 5 6 7 8 9 |
let tableView = UITableView(frame: .zero, style: .plain).with { $0.backgroundColor = .red $0.separatorColor = .green $0.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) $0.allowsMultipleSelection = true } |
We simply initialize an object we want to configure and call the with(:) function on the newly initilized object. The with(:) function takes only one parameter – a closure in which one has possibility to set up properties of the object. How does it look like in the code?
We created a protocol named Builder and in extension we created a default implementation of the with(:) function:
|
1 2 3 4 5 6 7 8 9 10 |
protocol Builder {} extension Builder { func with(configure: (Self) -> Void) -> Self { configure(self) return self } } |
The with(:) function is very simple – it takes configure closure as a parameter. The closure is immediately called with self which allows "configuring" self – e.g. setting properties on it. We also conformed NSObject to this protocol so that every subclass can use .with{} syntax:
|
1 2 3 4 5 6 7 8 9 10 11 |
extension NSObject: Builder {} let tableView = UITableView(frame: .zero, style: .plain).with { $0.backgroundColor = .red $0.separatorColor = .green $0.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) $0.allowsMultipleSelection = true } |
If we create a class that doesn’t inherit from NSObject we need to conform to the protocol manually:
|
1 2 3 4 5 6 |
class FooBar: Builder { var id: Int = 0 } |
We can check if our pattern works by using assertions from XCTest framework:
|
1 2 3 4 5 6 7 8 9 |
var foobar = FooBar().with { XCTAssertTrue($0.id == 0) //checks default value $0.id = 1 } XCTAssertTrue(foobar.id == 1) //checks value set in `configure` closure |
There is one problem with the presented approach. It doesn’t work for value types (i.e. struct).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct Foo: Builder { var id: Int = 0 } var foo = Foo().with { XCTAssertTrue($0.id == 0) $0.id = 1 //🚨 Cannot assign to property: '$0' is immutable 💥 } XCTAssertTrue(foo.id == 1) |
A struct given as a parameter to the configure closure is immutable by default. In order to make it mutable we need to use inout keyword and pass a reference to a type we want to configure. We want the syntax to work with class and struct types and we want to be able to assign returned type to a variable (as previously). So let’s create a BetterBuilder!
|
1 2 3 4 5 6 7 8 9 10 11 |
protocol BetterBuilder {} extension BetterBuilder { public func with(configure: (inout Self) -> Void) -> Self { var this = self configure(&this) return this } } |
Our BetterBuilder now works for structs!
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct Bar: BetterBuilder { var id: Int = 0 } var bar = Bar().with { XCTAssertTrue($0.id == 0) $0.id = 1 } XCTAssertTrue(bar.id == 1) |
And it still works for classes!
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Car: BetterBuilder { var id: Int = 0 } var car = Car().with { XCTAssertTrue($0.id == 0) $0.id = 1 } XCTAssertTrue(car.id == 1) |
Links
- Builder Pattern playground
- #41 Architecture Wars