Greetings, traveler!
We often use weak object references, for example, when using the Delegate design pattern. Consider the following example: We have a Manager which has a delegate. The Manager has an update function. This function notifies the delegate to perform several actions defined in the delegate method.
protocol ManagerDelegate: AnyObject {
func manager(_ manager: Manager, didUpdate value: String)
}
class Manager {
weak var delegate: ManagerDelegate?
func update(value: String) {
delegate?.manager(self, didUpdate: value)
}
}
Now let’s look at an example where a different implementation of the Manager should notify not one but several delegates at once.
class Manager {
weak var subscribers: [ManagerDelegate] = [] // ❌ Error
func update(value: String) {
subscribers.forEach {
$0.manager(self, didUpdate: value)
}
}
}
Here, we will encounter a compilation error:
‘weak’ may only be applied to class and class-bound protocol types, not ‘[any ManagerDelegate]’
Weak object wrapper
First, let’s create a wrapper for our object. Our wrapper will store a weak reference to the object. We can make it universal with a generic constraint. At the same time, we need to ensure that an object passed through the initializer has the reference type. To do this, we will limit the variations of objects with the AnyObject protocol, to which all reference types in Swift are implicitly confirmed.
final class WeakObject<T: AnyObject> {
private(set) weak var value: T?
init(_ value: T) {
self.value = value
}
}
But this solution is not very suitable for our case. We will encounter a problem when we want to add a new value to the delegate array.
class Manager {
var subscribers: [ManagerDelegate] = []
func update(value: String) {
subscribers.forEach {
$0.manager(self, didUpdate: value)
}
}
func subscribe(_ delegate: ManagerDelegate) {
let weakDelegate = WeakObject(delegate)
subscribers.append(weakDelegate) // ❌ Error
}
}
Argument type ‘WeakObject<any ManagerDelegate>’ does not conform to expected type ‘ManagerDelegate’
A weak array of protocols
To begin with, we need to move the verification for conformance with the AnyObject protocol to the initializer. Here, we will also capture a weak reference to the object.
final class WeakObject<T> {
var value: T? {
handler()
}
private let handler: () -> T?
init(_ value: T) {
let object = value as AnyObject
handler = { [weak object] in
object as? T
}
}
}
Now, let’s add some amenities. Property wrappers eliminate boilerplate code by placing part of the logic inside the wrapper object. They were introduced in 2019 at WWDC as part of Swift 5.1. Apple has also implemented property wrappers in its frameworks. SwiftUI, for example, makes extensive use of it.
Let’s create a property wrapper to wrap each array element in our wrapper class.
@propertyWrapper
struct WeakArray<Element> {
var wrappedValue: [Element] {
get {
storage.compactMap { $0.value }
}
set {
storage = newValue.map { WeakObject($0) }
}
}
private var storage = [WeakObject<Element>]()
}
Now, let’s use our property wrapper with our Manager.
final class Manager {
@WeakArray var subscribers: [ManagerDelegate]
func update(value: String) {
subscribers.forEach {
$0.manager(self, didUpdate: value)
}
}
func subscribe(_ delegate: ManagerDelegate) {
subscribers.append(delegate)
}
}
Conclusion
We have considered a convenient way to create and store a weak array of protocols. I hope you enjoyed this article. See you soon.