Getting to Swift.weak{}, part: 2

Scott Yelvington
8 min readNov 25, 2023

Continuing from my previous post (see here if you want to skip to the end and see the finished result). In order to build a boilerplate replacement for weak and unowned closures, we need to make a fundamental design decision about its foundation. Where are we going to situate the interface? We have one of two options:

  1. We can create global functions to solve this issue.
  2. We can create a public protocol and add a dispatch table to it.

The global route has one significant hitch: it doesn’t know what self is.

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

The global function would require passing in self as an argument in order to call the appropriate object. Because of the requirement for more information from the calling scope, it produces slightly less readable code. This is counter to the spirit of Swift, no?

Protocols and Dispatch Tables

Let’s start with the bones of our API. And add our first function to handle the above use case.

public protocol Retainable: AnyObject {
}

public extension Retainable {

typealias VoidVoidFunction = () -> Void

func weak(_ function: @escaping VoidVoidFunction) -> VoidVoidFunction {
{ [weak self] in
guard let self else { return }
function()
}
}
}

Wrapping closures in closures is the answer here. It’s entirely safe and allows us to interject interceding boilerplate logic. In this case, we want our function to capture another function, ensure the weak is safely unwrapped, and proceed. This will suit our needs, right? Wrong. Let’s annotate the flow when we use it to understand why:

func weak(_ function: @escaping VoidVoidFunction) -> VoidVoidFunction {
{ [weak self] in

guard let self else { return }

// Using `function` in the closure means the function and self
// is captured strongly. Self is also captured weakly by the closure
function()
}
}
api.get(
userId: someId,
completion: weak(self.handleResult)) // <-- This captures a self strongly

How do we get around this limitation? Even passing self into a weak capturing block causes a strong capture because passing the function signature means self is captured strongly. Well, we can create a function getting function. We only want to capture self and designate the correct capture type at the end of the day. What if we try it this way:

let functionGetter = { [weak self] in
return self?.handleResult
}

The closure functionGetter weakly captures self, and when called, returns a function signature as a closure that strongly retains self only if self hadn’t yet been deallocated when the closure is called. We could use this approach and incorporate this wrapper into the interface of our Retainable API, but that violates our first objective; it should be simple. So, how do we do this in a friendly, readable way?

Introspecting functions

This is where an existing language feature of Swift becomes quite powerful: introspection. Swift was designed from the ground up so that, at runtime, the language can inspect itself. It can look at the properties, the types of those properties, the functions, and the types of those functions and make decisions based on that information. Let’s use one of those introspective tools and make some adjustments to our interface. Pay close attention to the new syntax.

typealias VoidVoidFunctionGetter = (Self) -> () -> Void
typealias VoidVoidFunction = () -> Void

func weak(_ function: @escaping VoidVoidFunctionGetter) -> VoidVoidFunction {
{ [weak self] in

guard let self else { return }

function(self)()
}
}
// This is used like so:
api.get(
userId: someId,
completion: weak(ThisClass.handleResult)) // <-- self isn't being called now

Ok, you’re probably thinking, what the flip kinda dark magic was that? Every instance-level function you write has a less visible but ever-present equivalent function by the same name at the class level. That class-level function always has one argument (_ self: Self). The sole purpose of these static functions is to act as getters for the object-level functions. It’s a function getting function… get it?

If you write an object-level function named handleResult on an object of type ThisClass, the Swift lang will make a compile-time guarantee that a second handleResult function will be created at the class level, which receives an instance of the object and returns the object-level handleResult function reference. From there, you can create a weak capture inside a closure, use guard to unwrap safely, then pass self to your class level function getter along withself to get the function you’re after and run it. All the wrapper function needs to know is the type of the static function going in, and the type of the instance function coming out.

Introspecting properties

Still trying to wrap your head around it? That’s fine; let’s look at a different use case and circle back.

import SwiftUI

class ThisClass: Retainable, ObservableObject {

@Published
var userName: String = ""

lazy
var userNameBinding: Binding<String> = // ...
}

How might we create an unowned wrapper closure that gets and sets a value for some property? You may or may not be familiar with it, but all nominal types in Swift automatically come packaged with a subscript that allows us to, yes, introspect and dynamically access properties and sub-properties. It’s called keyPath.


typealias VoidOutputFunction<Output> = () -> Output
typealias InputVoidFunction<Input> = (Input) -> Void

func unowned<Output>(_ path: KeyPath<Self, Value>) -> VoidOutputFunction<Output> {
{ [unowned self] in
self[keyPath: path]
}
}

func unowned<Output>(_ path: ReferenceWritableKeyPath<Self, Value>) -> InputVoidFunction<Input> {
{ [unowned self] input in
self[keyPath: path] = input
}
}

We’ve just done it again! We’ve used the dark magic of introspection to separate needing to know the actual key path at the time of the read access from the code doing the actual reading. From the perspective of the code doing the reading, it just receives a path that matches the root type (aka Self) and the type one of the properties being accessed. This is the same as the function path we used earlier. In both cases, self is not strongly retained. Now let’s go back to our example.

import SwiftUI

class ThisClass: Retainable, ObservableObject {

@Published
var userName: String = ""

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

All we’ve done here is passed in a path that can be used on self, allowing the closure to capture self as unowned and use the path at a later time to dynamically read or write a new value on that path! There is no need for cumbersome closures doing the below.

  lazy
var userNameBinding: Binding<String> = .init(
get: { [unowned self] in
self.userName
},
set: { [unowned self] input in
self.userName = input
})

It halves the lines of code!

Bring on the syntax sugar, baby!

Now that we have the scaffolding of a solution sorted out, how do we add syntax sugar for things like defer, capture, set, and default? Let’s do some and flesh this out a bit more.

Defer:

typealias VoidVoidFunctionGetter = (Self) -> () -> Void
typealias VoidVoidFunction = () -> Void

func weak(
_ function: @escaping VoidVoidFunctionGetter,
defer deferring: VoidVoidFunction? = nil
) -> VoidVoidFunction {
{ [weak self] in

defer { deferring?() }

guard let self else { return }

function(self)()
}
}
api.get(
userId: someId,
completion: weak(
ThisClass.handleResult,
defer: weak(ThisClass.finishRequest)))

The above modification adds a deferred function. Using an optional type with a default parameter value of nil means we can leave it blank and add it to most of our interface functions without disruption.

Capture:

typealias InputVoidFunctionGetter<Input> = (Self) -> (Input) -> Void
typealias VoidVoidFunction = () -> Void

func weak<Context, Input>(
capture: Context,
_ function: @escaping InputVoidFunctionGetter<Input>,
defer deferring: VoidVoidFunction? = nil
) -> VoidVoidFunction {
{ [weak self] in

guard let self else { return }

function(self)(capture, input)

deferring?()
}
}
func handleResult(userId: String) {
}
api.get(
userId: someId,
completion: weak(
capture: someId,
ThisClass.handleResult))

If we want to replace closures with function references, we need to handle the fact that closures are great at capturing the surrounding context when needed. This is an easy addition; we’ll just add a new twist to the function that handles the added parameter on the incoming function, then exclude that parameter on the outgoing function.

Set:

typealias VoidOutputFunction<Output> = () -> Output
typealias VoidVoidFunction = () -> Void

func weak<Value>(
_ path: ReferenceWritableKeyPath<Self, Value>,
set value: Value
) -> VoidVoidFunction<Input> {
{ [weak self] in
self?[keyPath: path] = value
}
}
api.get(
userId: someId,
completion: weak(
ThisClass.handleResult,
deferring: weak(\.isComplete, set: true)))

We can incorporate a value to capture and set using our nifty key path syntax!

Default:

typealias InputOutputFunctionGetter<Input, Output> = (Self) -> (Input) -> Output
typealias InputOutputFunction<Input, Output> = (Input) -> Output

func weak<Input, Output>(
_ function: @escaping InputOutputFunctionGetter<Input, Output>,
default defaultValue: Output
) -> InputOutputFunction<Input, Output> {
{ [weak self] input in

guard let self else { return defaultValue }

return function(self)(input)
}
}
// returns true on success!
func handleResult(userId: String) -> Bool {
}
api.get(
userId: someId,
completion: weak(
ThisClass.handleResult,
default: true))

Since some closures are expected to return results, and since we’re retaining weakly, why not support returning a default result if weak is false? We can even change the incoming function’s output type to optional and use the same default syntax as a nil coalescing equivalent.

Conclusion

By extending a public protocol that inherits from AnyObject, our wrapper functions know who self is. By wrapping our closures in a closure, we can add interceding logic, which can safely unwrap self. Once we’ve done this, we can use the power of introspection to simplify the interface further, allowing us to pass paths to functions or key paths to be called or modified in a later context. If you’re interested, you can have a look at examples in a public repo here.

But as clean as this already is, is it as clean as the code we were after in part 1? Remember, we wanted it to be as clean as some of the examples below:

// we want this:
api.get(
userId: someId,
completion: weak(handleResult))

// as opposed to this:
api.get(
userId: someId,
completion: weak(ThisClass.handleResult))
// we still want this:
DispatchQueue
.main
.asyncAfter(
deadline: .now() + .seconds(30),
execute: weak(endTimer))

// vs this:
DispatchQueue
.main
.asyncAfter(
deadline: .now() + .seconds(30),
execute: weak(ThisClass.endTimer))
// finally, we want this:
self.completionHandler = weak {

self.doSomething()
}

// instead of this:
self.completionHandler = weak(ThisClass.doSomething)

But those first examples are passing strong self, right? Well… only if you’re too squeamish to manage memory in a mostly automated runtime. And if you are, then for the purposes of killing off boilerplate, the interface we created here in Part 2 will do just fine. However, if we wanted to make it cleaner, we have to contend with the fact that the boilerplate we’re trying to eliminate is ARC itself! To do that, we need to grapple with what ARC is actually doing to manage the lifecycle of a closure, as well as ponder what closures actually are. Which we’ll dive into in part three ✌🏻

--

--