I've solved this issue for myself by using a special container type that can store a weakly held object alongside the closure. The trick that is used by the container type is to check if the object still exists before calling the closure. If the object still exist, it is passes it as a non-optional parameter to the closure. If it doesn't exist anymore, the closure isn't called at all.
As an example, consider a ViewController with a button that has an onTap handler:
class ViewController: UIViewController {
let button = MyButton()
override func viewDidLoad() {
(...)
button.onTap(with: self) { vc in // only called if self still exists
vc.doSomething()
}
}
}
To make this work MyButton uses the CallbackStore container type:
class MyButton {
let onTap = CallbackStore<()>() // In this example we don't pass an extra parameter to the callback closure, hence the () type parameter
func touchUpInside() {
onTap.executeAll() // Executes all callback closures
}
}
Incidentally, the implementation of CallbackStore contains the first (and so far only) use I found for callAsFunction.
CallbackStore.swift
public class CallbackStore<P> {
fileprivate var callbacks: [CallbackStoreItem]
public init() {
callbacks = []
}
public func callAsFunction<O: AnyObject>(with object: O, block: @escaping (O) -> ()) {
callbacks.append( WeakItem(owner: object, block: block))
}
public func callAsFunction<O: AnyObject>(with object: O, block: @escaping (O, P) -> ()) {
callbacks.append( WeakParameterItem(owner: object, block: block))
}
public func callAsFunction(block: @escaping () -> ()) {
callbacks.append( StrongItem(block: block))
}
public func callAsFunction(block: @escaping (P) -> ()) {
callbacks.append( StrongParameterItem(block: block))
}
public func executeAll(value: P) {
callbacks.forEach { $0.execute(value: value) }
}
}
extension CallbackStore where P == () {
public func executeAll() {
executeAll(value: ())
}
}
public class CallbackStore2<P1, P2>: CallbackStore<(P1, P2)> {
public func callAsFunction<O: AnyObject>(with object: O, block: @escaping (O, P1, P2) -> ()) {
callbacks.append( WeakParameterItem(owner: object) { (object: O, tuple: (P1, P2)) in
block(object, tuple.0, tuple.1)
})
}
public func callAsFunction(block: @escaping (P1, P2) -> ()) {
callbacks.append( StrongParameterItem(block: { (tuple: (P1, P2)) in
block(tuple.0, tuple.1)
}))
}
}
public class CallbackStore3<P1, P2, P3>: CallbackStore<(P1, P2, P3)> {
public func callAsFunction<O: AnyObject>(with object: O, block: @escaping (O, P1, P2, P3) -> ()) {
callbacks.append( WeakParameterItem(owner: object) { (object: O, tuple: (P1, P2, P3)) in
block(object, tuple.0, tuple.1, tuple.2)
})
}
public func callAsFunction(block: @escaping (P1, P2, P3) -> ()) {
callbacks.append( StrongParameterItem(block: { (tuple: (P1, P2, P3)) in
block(tuple.0, tuple.1, tuple.2)
}))
}
}
// Private types
fileprivate protocol CallbackStoreItem {
func execute(value: Any)
}
fileprivate struct WeakItem<O: AnyObject>: CallbackStoreItem {
weak var owner: O?
let block: (O) -> ()
func execute(value: Any) {
if let owner = owner {
block(owner)
}
}
}
fileprivate struct WeakParameterItem<O: AnyObject, T>: CallbackStoreItem {
weak var owner: O?
let block: (O, T) -> ()
func execute(value: Any) {
if let owner = owner, let value = value as? T {
block(owner, value)
}
}
}
fileprivate struct StrongItem: CallbackStoreItem {
let block: () -> ()
func execute(value: Any) {
block()
}
}
fileprivate struct StrongParameterItem<T>: CallbackStoreItem {
let block: (T) -> ()
func execute(value: Any) {
if let value = value as? T {
block(value)
}
}
}