Escaping closure management

Hi everyone,

Right now I'm building some kind of event dispatcher using closure (many-> one relationship).
I decided to go with a collection of escaping closures, everything work fine. However, I was wondering what is the best way to remove a specific closure from the collection.

I thought of multiple option but I'm not convinced by any of them.

For now, I use an Array of closure, which doesn't allow me to remove a closure since closures can not be compared I can't know which one is which.
I though about using a dictionary, but then I need to use a reference of some sort for the key that complexifies the usage of the event dispatcher.

So my questions are :

What would be the best approach for such a problem in swift.
Are closures even the right tool for that?

Thank you a lot

I've previously used an API like this:

typealias Handler = () -> Void
private var eventHandlers: [UUID: Handler] = [:]

@discardableResult
public func addHandler(_ handler: @escaping Handler) -> UUID {
    let id = UUID()
    eventHandlers[id] = handler
    return id
}

public func removeHandler(id: UUID) {
    eventHandlers.removeValue(forKey: id)
}

Prior to Combine, I used an EquatableClosure type. Now there's Combine, so I'd just use Combine.

I would like too but I'm targeting devices where combine is not an option... unfortunately

However, this is also for me the occasion to find/understand the swift way :)

Thanks, that what I was thinking to do, but that doesn't feel right to me

You may find Equality of functions to be an interesting read.

But for now, I cannot think of anything dramatically different from what @sveinhal suggested. The only thing I can add to that, is that I would suggest using object reference instead of UUID. Memory allocation comes with a unique Iā€™d generator built-in. As a bonus you can automatically unsubscribe on deinit of the returned object.

2 Likes

Using an object is good, but you should also think about genericizing over parameters.

Really though, it's better to just copy the pieces of Combine that you need if you can't use Combine.

/// A workaround for Swift not providing a way to remove closures
/// from a collection of closures.
///- Note: Designed for one-to-many events, hence no return value.
///  Returning a single value from multiple closures doesn't make sense.
public final class MultiClosure<Input>: Equatable {
  public init(_ closures: EquatableClosure<Input>...) {
    self += closures
  }

  public init<Closures: Sequence>(_ closures: Closures)
  where Closures.Element == EquatableClosure<Input> {
    self += closures
  }

  var closures: Set<
    UnownedReferencer< EquatableClosure<Input> >
  > = []

// MARK: deallocation
  // We can't find self in `closures` without this.
  fileprivate lazy var unownedSelf = UnownedReferencer(self)
  
  // Even though this MultiClosure will be deallocated,
  // its corresponding WeakReferencers won't be,
  // unless we take this manual action or similar.
  deinit {
    for closure in closures {
      closure.reference.multiClosures.remove(unownedSelf)
    }
  }
}

public extension MultiClosure {
  /// Execute every closure
  func callAsFunction(_ input: Input) {
    for closure in closures {
      closure.reference(input)
    }
  }
}

public extension MultiClosure where Input == () {
  /// Execute every closure
  func callAsFunction() {
    self(())
  }
}

/// A wrapper around a closure, for use with MultiClosures
public final class EquatableClosure<Input>: Equatable {
  public init(_ closure: @escaping (Input) -> Void) {
    self.closure = closure
  }

  /// Execute the closure
  func callAsFunction(_ input: Input) {
    closure(input)
  }

  private let closure: (Input) -> Void

// MARK: deallocation
  var multiClosures: Set<
    UnownedReferencer< MultiClosure<Input> >
  > = []

  // We can't find self in `multiClosures` without this.
  fileprivate lazy var unownedSelf = UnownedReferencer(self)
  
  deinit {
    for multiClosure in multiClosures {
      multiClosure.reference.closures.remove(unownedSelf)
    }
  }
}

/// Add `closure` to the set of closures that runs
/// when `multiClosure` does
public func += <Input>(
  multiClosure: MultiClosure<Input>,
  closure: EquatableClosure<Input>
) {
  multiClosure.closures.insert(closure.unownedSelf)
  closure.multiClosures.insert(multiClosure.unownedSelf)
}

/// Add `closures` to the set of closures that runs
/// when `multiClosure` does
public func += <
  Input,
  Closures: Sequence
>(
  multiClosure: MultiClosure<Input>,
  closures: Closures
)
where Closures.Element == EquatableClosure<Input> {
  for closure in closures {
    multiClosure += closure
  }
}

/// Remove `closure` from the set of closures that runs
/// when `multiClosure` does
public func -= <Input>(
  multiClosure: MultiClosure<Input>,
  closure: EquatableClosure<Input>
) {
  multiClosure.closures.remove(closure.unownedSelf)
  closure.multiClosures.remove(multiClosure.unownedSelf)
}