Cleaning up in deinit with self, and complete concurrency checking

EDIT: I was wrong here

I guess that will be incorrect once this proposal is accepted as one would be able to send Trampoline to another isolation domain regardless of its sendability.
What we need here is the above mentioned Isolated synchronous deinit, but there weren't any updates on that in past two years. So I guess it would be better to implement a workaround on your side than wait.
I think this can be solved by introduction another private object to interact with UIControl and move the call to removeTarget from deinit into a method, so it will be isolated. This object should be held by the outer object (Trampoline) and never leaked out. Something like


public extension UIControl {
  @MainActor
  func observe(event: UIControl.Event, _ block: @escaping @MainActor (UIControl) -> Void) -> Any {
    return Trampoline(observee: self, event: event, block: block)
  }
}

private final class Trampoline {
  @MainActor
  final class Inner {
    private weak var trampoline: Trampoline?
    private weak var observee: UIControl?

    init(observee: UIControl, event: UIControl.Event, trampoline: Trampoline) {
      self.trampoline = trampoline
      self.observee = observee
      observee.addTarget(self, action: #selector(Inner.bounce(sender:)), for: event)
    }

    @objc func bounce(sender: UIControl) {
      trampoline?.block(sender)
    }

    func unsubscribe() {
      observee?.removeTarget(self, action: nil, for: .allEvents)
    }
  }

  private let block: @MainActor (UIControl) -> Void
  private var inner: Inner!

  @MainActor
  init(observee: UIControl, event: UIControl.Event, block: @escaping @MainActor (UIControl) -> Void) {
    self.block = block
    inner = Inner(observee: observee, event: event, trampoline: self)
  }

  deinit {
    Task { @MainActor [inner = inner!] in
      inner.unsubscribe()
    }
  }
}

(Be careful, I haven't tested this)

2 Likes