A meta library to provide a better `Delegate` pattern
Delegate
A meta library to provide a better Delegate
pattern described here and here.
Usage
Instead of a regular Apple’s protocol-delegate pattern, use a simple Delegate
object to communicate:
class ClassA {
let onDone = Delegate<(), Void>()
func doSomething() {
// ...
onDone()
}
}
class MyClass {
func askClassAToDoSomething() {
let a = ClassA()
a.onDone.delegate(on: self) { (self) in
self.jobDone()
}
a.doSomething()
}
private func jobDone() {
print("?")
}
}
Why
Compare to regular delegation
Delegate
does the same thing with much less code and compact structure. Just compare with the same work above in a formal protocol-delegate pattern.
protocol ClassADelegate {
func doSomethingIsDone()
}
class ClassA {
weak var delegate: ClassADelegate?
func doSomething() {
// ...
delegate?.doSomethingIsDone()
}
}
class MyClass {
func askClassAToDoSomething() {
let a = ClassA()
a.delegate = self
a.doSomething()
}
private func jobDone() {
print("?")
}
}
extension MyClass: ClassADelegate {
func doSomethingIsDone() {
self.jobDone()
}
}
No one loves to write boilerplate code, do you?
Compared to onXXX
property
At the first glance, you may think Delegate
is an over-work and can be replaced by a stored property like this:
class ClassA {
var onDoneProperty: (() -> Void)?
//...
}
It creates a strong holding, and I found it is really easy to create an unexpected cycle:
class MyClass {
var a: ClassA = ClassA()
func askClassAToDoSomething() {
a.onDoneProperty = {
// Retain cycle!!
self.jobDone()
}
}
You have to remember [weak self]
for most cases to break the cycle, it also requires you to check self
before using it:
class MyClass {
var a: ClassA = ClassA()
func askClassAToDoSomething() {
a.onDoneProperty = { [weak self] in
guard let self = self else { return }
self.jobDone()
}
}
Boilerplate code again! And things would become more complicated if the onDoneProperty
needs to holds caller across multiple layers.
How
Delegate
holds the target
in a weak
way internally, and provide a “strongified” shadowed version of the target to
you when the delegate is called. So you can get the correct memory management for free and focus on your work with no boilerplate code at all.
a.onDone.delegate(on: self) { // This `self` is the delegation target. `onDone` holds a weak ref of it.
(self) in // This `self` is a shadowed, non-option type.
self.jobDone() // Using of this `self` does not create retain cycle.
}
To pass some parameters or receive a return type, just declared the Delegate
‘s generic types:
class DataController {
let onShouldShowAtIndexPath = Delegate<IndexPath, Bool>()
func foo() {
let currentIndexPath: IndexPath = // ...
let shouldShow: Bool = onShouldShowAtIndexPath(currentIndexPath)
if shouldShow {
show()
}
}
}
// Caller Side
dataSource.onShouldShowAtIndexPath.delegate(on: self /* : Target */ ) { (self, indexPath) in
// This block has a type of `(Target, IndexPath) -> Bool`.
return indexPath.row != 0
}
Caution
The only caution is, please always use the shadowed self
in the delegation block. Say, this would cause a
regression to the old onXXX
property way and causes a retain cycle:
a.onDone.delegate(on: self) { (_) in
self.jobDone()
}
It seems that you can use the “same” self
, but actually in the code above you are using the “real” strong self
. Do not
mark the first input parameter of block as _
and always give it a name of self
then you can prevent this.