No more [weak self], or the weird new future of delegation
Ready for some property wrappers?
It’s been almost three years since I wrote ”Do you often forget [weak self]? Here’s a solution”, which luckily resonated with many people. I’ll do a short recap, but if you want a deeper understanding of the topic you can go ahead and read that article first.
In short, the problem I was trying to solve was this: since traditional “protocol-based” delegation was seen by many (including me) as too cumbersome, alternative “closure-based” delegation was gaining traction. The concept is easy and, in pseudo-UIKit-code, looks something like this:
final class TextField {
var didUpdate: (String) -> () = { _ in }
/// ...
private func didFinishEditing() {
didUpdate(self.text)
}
}
final class ViewController {
let label = Label()
let textField = TextField()
func viewDidLoad() {
textField.didUpdate = { text in
self.label.text = text
}
}
}
This is neat and easy, but also very very tricky: see how we just introduced a retain cycle? ViewController holds a strong reference to TextField, and the TextField’s didUpdate
now holds a strong reference to a ViewController. Which is why it’s always necessary in these cases to use [weak self]
:
func viewDidLoad() {
textField.didUpdate = { [weak self] text in
self?.label.text = text
}
}
This is, of course, extremely easy to miss or forget, and the next moment you’re spending 40+ minutes trying to debug your memory leak.
My solution was to shift the responsibility to include [weak self]
from the API user (ViewController), to the API designer (TextField), using this structure:
struct Delegated<Input> {
private(set) var callback: ((Input) -> Void)?
mutating func delegate<Object : AnyObject>(
to object: Object,
with callback: @escaping (Object, Input) -> Void
){
self.callback = { [weak object] input in
guard let object = object else {
return
}
callback(object, input)
}
}
}
(read the original article for more details)
And the code now looks like this:
final class TextField {
var didUpdate = Delegated<String>()
/// ...
private func didFinishEditing() {
didUpdate.callback?(self.text)
}
}
final class ViewController {
let label = Label()
let textField = TextField()
func viewDidLoad() {
textField.didUpdate.delegate(to: self) { (self, text) in
self.label.text = text
}
}
}
So now didUpdate
itself makes sure that no unwanted retain cycles are introduced, which is just what we wanted.
And that was pretty much it.
It was a neat little trick, and generated a good amount of positive feedback, as well as some very fair debate over the stylistic choice, especially the shadowing of self
.
I myself adopted the pattern in all my side-projects, but didn’t really expect anyone else to jump onboard. Arguably, using the Delegated<Int>
instead of (Int) -> Void
was a hard sell. It looked weird and unintuitive.
So here comes today. And I’m back with an even weirder solution. Let me present to you: Delegated 2.0.
@propertyWrapper
public final class Delegated<Input> {
public init() {
self.callback = { _ in }
}
private var callback: (Input) -> Void
public var wrappedValue: (Input) -> Void {
return callback
}
public var projectedValue: Delegated<Input> {
return self
}
func delegate<Target: AnyObject>(
to target: Target,
with callback: @escaping (Target, Input) -> Void
) {
self.callback = { [weak target] input in
guard let target = target else {
return
}
return callback(target, input)
}
}
}
…yeah, now a property wrapper.
So what changed? Let’s go back to our snippet and update it with the new shiny Delegated property wrapper:
final class TextField {
@Delegated var didUpdate: (String) -> Void
/// ...
private func didFinishEditing() {
self.didUpdate(self.text)
}
}
final class ViewController {
let label = Label()
let textField = TextField()
func viewDidLoad() {
textField.$didUpdate.delegate(to: self) { (self, text) in
self.label.text = text
}
}
}
And okay, if you’re rolling your eyes right now, I hear you. This seems somewhat ridiculous. But also… beautiful? Just see how natural this looks. This is so, so similar to the original “closure-based” delegation with all its simplicity, but now also with the additional benefit of safety.
You can make up your own mind about whether you like it or hate it (and please let me know in the comments!). I’ve been using this for a few months (including in both Ask Yourself Everyday app & Time and Again) and I am loving it. It’s a huge productivity booster (the boilerplate of writing protocol-based delegation is just too much), and with this I don’t need to think about [weak self]
ever, which is lovely.
Caveats
With Swift generics being somewhat limited, there are still of course some drawbacks to this solution. First and foremost — only closures that have exactly one argument and no return value can be marked as @Delegated
. Which means that this will not compile:
final class Button {
@Delegated var didPress: () -> Void
}
final class ScrollView {
@Delegated var didScrollTo: (_ x: CGFloat, _ y: CGFloat) -> Void
}
But this will:
final class Button {
@Delegated var didPress: (()) -> Void
}
final class ScrollView {
@Delegated var didScrollTo: ((x: CGFloat, y: CGFloat)) -> Void
}
Which honestly makes me sad. It’s one of my biggest pet peeves with Swift.
Alternative (although not much prettier) solution is to create additional property wrappers for different number of arguments. For example, Delegated0
:
@propertyWrapper
public final class Delegated0 {
public init() {
self.callback = { }
}
private var callback: () -> Void
public var wrappedValue: () -> Void {
return callback
}
public var projectedValue: Delegated0 {
return self
}
func delegate<Target: AnyObject>(
to target: Target,
with callback: @escaping (Target) -> Void
) {
self.callback = { [weak target] in
guard let target = target else {
return
}
return callback(target)
}
}
}
After creating a few of these, you’ll have this:
final class Button {
@Delegated0 var didPress: () -> Void
}
final class ScrollView {
@Delegated2 var didScrollTo: (_ x: CGFloat, _ y: CGFloat) -> ()
}
Don’t blame me, blame Swift.
It’s not necessary (you can just write it yourself), but if anything, the new @Delegated
property wrapper is available as a Swift package. It includes base @Delegated
as well as @Delegated0
— @Delegated4
, which you might or might not need. Check it out:
Thanks for reading the post! Don’t hesitate to ask or suggest anything in the “responses” section below. You can also contact me on Twitter or find me on GitHub. If you have written a piece (or stumbled upon one) exploring similar topic — make sure to post a link to it in the responses so I can include it right below.
Further learning:
Hi! If you want to support me, please check out my apps: “Ask Yourself Everyday”,“Time and Again” and “Women’s Football 2017”. For business inquiries, reach me at oleg@dreyman.dev. Thanks for reading!