Delegation is a simple and powerful pattern in which one object acts on behalf of another object by holding a reference to the delegate, then sending messages through聽it. This is the pattern Apple has chosen for its frameworks and has worked quite well… a little awkward at first, but eventually makes sense 馃檪
Then聽Swift came along and brought new techniques and patterns due to it’s functional paradigm. The game changed and Apple’s coding conventions are making a slow shift to becoming聽more Swifty.聽In this post, I’m going to illustrate how we can convert聽delegates to聽a closures-based pattern.
What About Delegates?
Although delegates are powerful, it’s a bit archaic.聽With reactive and event-driven programing, delegates聽breaks down and lose its elegance.聽See this聽simple delegate pattern example below. Notice聽how the completed
聽flag gets propagated back to the parent object who was passed into the Todo
聽initializer:
protocol TodoDelegate: class { func completed(todo: Todo) } struct Todo { var id: Int var title: String var completed: Bool = false { didSet { // Notify delegate of completion guard completed else { return } delegate?.completed(todo: self) } } weak var delegate : TodoDelegate? init(_ delegate: TodoDelegate, id: Int, title: String) { self.delegate = delegate self.id = id self.title = title } } class MyParentController: TodoDelegate { lazy var todo1: Todo = { return Todo(self, id: 1, title: "Todo item 1") }() lazy var todo2: Todo = { return Todo(self, id: 2, title: "Todo item 2") }() lazy var todo3: Todo = { return Todo(self, id: 3, title: "Todo item 3") }() func completed(todo: Todo) { switch todo.id { case 1: print("Do something with todo: \(todo.title)") case 2: print("Do another thing with todo: \(todo.title)") case 3: print("Do final thing with todo: \(todo.title)") default: break } } } let controller = MyParentController() controller.todo1.completed = true controller.todo2.completed = true controller.todo3.completed = true // Prints the following to the console: // Do something with todo: Todo item 1 // Do something with todo: Todo item 1 // Do final thing with todo: Todo item 3
The Todo
聽objects were聽instantiated in聽MyParentController
聽and passed itself in as the delegate. It accepted聽the delegate as聽a specific protocol called TodoDelegate
. This protocol will indicate to the Todo
聽object what properties and functions the聽delegate has.
In the complete
聽property of Todo
, the didSet
聽event calls the delegate’s completed
聽function. It knows that the聽completed
聽function is there because MyParentController
聽adopts TodoDelegate
.
This聽is聽where delegates聽breaks down. The completed
聽function in MyParentController
聽is being shared for all Todo
聽objects. I have to put a switch
聽statement there to find the one who triggered it and perform the customized action for it.聽What if we we’re listening to聽hundreds of聽Todo
聽objects, that switch
聽statement would get out of control. Even besides this, it feels weird聽that the Todo
聽object and the triggered function are in two different places.
Delegates to Closure Pattern
We will now convert the above delegate pattern to a closure-based one. This fits more into the functional programming paradigm. Instead of a delegate function getting聽triggered, we will allow callers to “subscribe” to the聽Todo
聽object. The subscriptions will take a closure and queue it for later execution when ready:
struct Todo { var id: Int var title: String var completed: Bool = false { didSet { // Notify subscribers of completion guard completed else { return } handlers.forEach { $0(self) } } } // Task queue var handlers = [(Todo) -> Void]() init(id: Int, title: String) { self.id = id self.title = title } mutating func subscribe(completion: @escaping (Todo) -> Void) { handlers += [completion] } } class MyParentController { lazy var todo1: Todo = { return Todo(id: 1, title: "Todo item 1") }() lazy var todo2: Todo = { return Todo(id: 2, title: "Todo item 2") }() lazy var todo3: Todo = { return Todo(id: 3, title: "Todo item 3") }() } let controller = MyParentController() controller.todo1.subscribe { print("Do something with todo: \($0.title)") } controller.todo2.subscribe { print("Do another thing with todo: \($0.title)") } controller.todo3.subscribe { print("Do final thing with todo: \($0.title)") } controller.todo1.subscribe { print("Another one for fun with todo: \($0.title)") } controller.todo1.completed = true controller.todo2.completed = true controller.todo3.completed = true // Prints the following to the console: // Do something with todo: Todo item 1 // Another one for fun with todo: Todo item 1 // Do another thing with todo: Todo item 1 // Do final thing with todo: Todo item 3
We got rid of the delegate protocol completely. In fact, the Todo
聽objects doesn’t聽even need a reference to聽MyParentController
聽at all!聽Instead, the caller subscribes to the todo’s events:
controller.todo1.subscribe { print("Do something with todo: \($0.title)") }
This is great because now the focus聽is on the Todo
聽object. It makes more sense to attach a closure to the Todo
聽object itself instead of defining it somewhere else in a shared delegate function.
During the subscribe
聽process, it stores this closure into a queue to be called later when ready:
var handlers = [(Todo) -> Void]() ... func subscribe(completion: @escaping (Todo) -> Void) { handlers += [completion] }
When it’s time to trigger the event, it executes the closures聽in the queue one by one:
var completed: Bool = false { didSet { guard completed else { return } handlers.forEach { $0(self) } } }
What About Thread-Safety?
There’s one complication you must handle. The queue is simply an array of closures. This can be dangerous if subscriptions were to happen from multiple threads. Therefore, we need some locking or exclusivity mechanism on the queue. The good news is I have already done this in another post called Creating Thread-Safe Arrays in Swift.聽We can use the SynchronizedArray
聽from there for our queue type:
var handlers = SynchronizedArray<(Todo) -> Void>() ... func subscribe(completion: @escaping (Todo) -> Void) { handlers += [completion] }
Everything else stays the same because the SynchronizedArray
聽has many of the same API’s as a regular array, but is thread-safe!
Conclusion
Although this was a simple example, the delegate to closure pattern can be applied to more complex scenarios.聽For example, imagine you can subscribe to GPS location updates instead of sharing a single delegate function:
locationManager.subscribeLocation { print("Location is: \($0)") }
This becomes even more useful for shared singletons which聽can only accept one delegate,聽instead of being able to trigger multiple delegates. In a future post, I’ll convert delegates to closures for native Apple managers like聽CLLocationManager聽and聽CBCentralManager. With a thread-safe queue in place, working with them should聽feel Swifty and robust.
Happy Coding!!
Hi Basem,
Thanks for the valuable articles.
A couple of points/questions:
1. Why handlers += [completion] rather than handlers.append(completion) ? Just a matter of preference?
2. You gave todo2 an id of 1 and titled it as 1 also. Don’t you mean to give it id 2 and title it as such?
Finally:
It occurred to me that, since you subscribe to each Todo anyway, there is an opportunity to compact the code somewhat by allowing for an initial subscription when initialising the Todo:
init(id: Int, title: String, initialSubscription: @escaping (Todo) -> Void = {_ in}) {
self.id = id
self.title = title
subscribe(completion: initialSubscription)
}
You can then forego the lines where you explicitly subscribe, by deleting:
controller.todo1.subscribe {
print(“Do something with todo: \($0.title)”)
} …etc…
instead creating your todos like this:
lazy var todo1: Todo = {
return Todo(id: 1, title: “Todo item 1”) {
print(“Do something with todo: \($0.title)”)
}
}()
Note that since the initializer provides a default value for the closure, you can still create Todos without a subscription:
lazy var todo4: Todo = {
return Todo(id: 4, title: “Todo item 4”)
}()
Or make the closure an optional, rather than providing a default value, in which case it becomes implicitly escaping:
init(id: Int, title: String, initialSubscription: ((Todo) -> Void)? = nil) {
self.id = id
self.title = title
if let action = initialSubscription {
subscribe(completion: action)
}
}
Thanks Ant贸nio for the feedback and fixes!
Whoops you’re right about `todo2` and I updated the post with the fixed property details.
Regarding the `handlers += [completion]` syntax, it was indeed a matter of preference and created the infix operator for convenience. I picked this up from C# btw.
You are correct about adding a default closure to the initialization of `todo`, which would give you the same effect. It actually makes more sense in that case. The example I gave may not fully capture the intent though. Say instead of subscribing to changes of an instance, you’d want to subscribe to changes triggered by an event or state in the system. The next article I wrote about converting delegates to closures for the `CLLocationManager` is probably a better, real-world example.
outstanding!
How to unsubscribe ?
You need to assign a unique identifier to each subscription, store it in memory, and use it to unsubscribe. See this for a more fuller example: http://basememara.com/swifty-locations-observables/