Getting to Swift.weak { }, part: 1

Scott Yelvington
5 min readNov 18, 2023

--

Over the last 7 or so years, I alongside many of you good people have watched on longingly as Apple has taken our mighty community from the days of much tedium in the lands of Obj-C yore to the promised highways and byways of Swift, the performant, safe, and speedy way to develop our apps, widgets, and other such tomfooleries.

With each successive release of Swift, Apple has given us new language level support to make our code cleaner to read and faster to write. And yes, these things indeed are all good. And lo the people were happy and did frollock when 5.8 brought us Async/Await. And there were many rejoicings. But the land was naught without one feature. A boilerplate was yet unaddressed. One that still flummixes the wills and gnashes the teeth of — — ok I can’t keep writing like this. Listen here’s the deal. Why can’t we get a cleaner way to weak retain self in functions?! I know I know, it’s already great. But it could be better amirite? Let me explain.

A few nights back I found myself building out boilerplate API callbacks and realized I was doing something over and over that I’ve yet to substiantively solve. I’ve written code snippets. I’ve also… well that’s about it. I refer you now to exhibit A:

api.get(userId: someId) { [weak self] result in 
guard let self else { return }
...
}

On this auspicious evening, I realized I had waited to write all of my API requests in one night. And so, I began, again and again and again, writing the same biolerplate phrase until I wanted to cry. I heard it again and again as I typed it “Weak self result in guard let self {}. Weak self result in guard let self {}.” And on and on. Why is there no better way? I decided to break and search the web once more to see if anyone had finally written a way to kill this boilerplate yet. But of course, as I had last checked, the answer was sadly the same.

Many articles warned me not to do this:

api.get(
userId: someId,
completion: self.handleResult) // <-- this is a strong retain

Lock me up now because I LOVE passing function pointers around and I’d do it any chance I get. Of course, when one knows what they’re doing, it’s perfectly acceptable and totally safe if you don’t mind strongly retaining your object. But because it’s not safe in some contexts it’s wise to make it an antipattern… Unless you can make it safe in all contexts.

Some folks boldly wrote global functions that are used like so:

api.get(
userId: someId,
completion: weakify(self, type(of: self).handleResult))

And listen, I’m not throwing shade, I admire the resourcefulness of the insanely creative people in our community. But I wanted it to be simpler, cleaner. So I started coding something that took me into the bowls of ARC and Swift Closures. That journey served as a nifty reminder as to how all of nuts and bolts fit together.

First, let’s set a goal for the interface we’d like to use because let’s be real, by the end of our careers, we will probably write hundreds of thousands of closures and blocks. Some of us may write millions. So here are my requirements:

  1. It should be simple. We want fewer keystrokes than your traditional weak retain closure capture syntax.
  2. It should be fast to write. The point here is to generate less boilerplate per closure; not simply to replace some boilerplate with some equally dense boiler plate.
  3. It should be versatile. Closures are used in many contexts. So why not make it easy to use in most corner cases involving callbacks and closures?

Use cases

api.get(
userId: someId,
completion: weak(self.handleResult))

// OR even better:

api.get(
userId: someId,
completion: weak(handleResult))

The ability to pass a strong function pointer is a powerful tool that simplifies your code and makes it more readable. But passing a function pointer to an object’s function in Swift guarantees it’s retained strongly until the pointer itself is released. We want to add language level support for passing weak ones. If we can unlock that capability, this helps the functional coders among us break up their code. Along those lines, I’d also like to add the following:

api.get(
userId: userId,
completion: weak(capture: userId, self.handleResult))

That’s right, Swift closures, just like Objective-C closures can retain objects, structs, and values from the context they were created in. We don’t want to lose that do we? And how about this:

// the simple way
button.onPress(strong(doSomething))
button.onPress(weak(doSomething))
button.onPress(unowned(doSomething))

// and with the capture
button.onPress(strong(capture: userId, doSomething))
button.onPress(weak(capture: userId, doSomething))
button.onPress(unowned(capture: userId, doSomething))

Listen, as long as that button exists, self should exist. And those of us who are picky know there are three reference pointer types in Swift: strong, weak, and unowned. We want to be able to make use of all three.

Many times closures assign to properties alone. For instance, in SwiftUI, my ViewModels are represented to the UI by protocols for easy mocking and testing. Take this property on a ViewModel for example:


class ViewModel: ObservableObject {

@Published
var userName: String = ""
@Published
var currentEmail: String = ""

lazy
var userNameBinding: Binding<String> = .init(
get: unowned(\.userName),
set: unowned(\.userName))
lazy
var emailBinding: Binding<String> = .init(
get: unowned(\.currentEmail),
set: unowned(regexEmailInput))

Call me opinionated, but I feel that key paths are vastly underutilized by Swift developers. Much closure boilerplate that is unnecessary often performs simple tasks like calling a single function or updating a single property. Let’s support both! In the above, the first binding updates a single property on the object. On the second binding, the API could use a function as a setter to perform regex.

In both cases, some views and view models contain scores of properties that contain small bits of interceding logic (think form views). The boilerplate code here would make our files rather large, unreadable, and error prone. Slip up with one of these and you’ve done it and created a retain cycle. And if we multiply the four lines it took to create this by 2x and say we have 10 such properties, that’s something like 80 lines of boilerplate, hard-to-read swift code this could save us.

We might also want to defer some action to the end of the code flow.

api.get(
userId: someId,
completion: weak(
handleResult,
defer: {
...
})

// we can even make our deferred action weak like so:
api.get(
userId: someId,
completion: weak(
handleResult,
defer: weak(\.isCompleted, set: true)

And finally, saving the best for last, we want to get down to the core use-case of the closure in Swift: the ability to pass code around capturing scope and call said code in a different context aka the following:

// we want this

obj1.eventHandler = weak () { event in

self.doSomething(with: event)
}

// rather than this

obj1.eventHandler = { [weak self] event in

guard let self else { return }

self.doSomething(with: event)
}

Conclusion

In order to kill the boilerplate in all of these use cases, one has to dive vertical into the nature of ARC, Closures, and the substance of Swift. I plan to post a multipart explainer that fleshes out the journey I took to solve all of these use-cases and explains what ARC, Swift, and functions/closures are doing under the hood. Join me for Part 2, fellow journeyer!

--

--