Skip to content

Instantly share code, notes, and snippets.

@blixt
Last active January 19, 2023 19:42
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blixt/08434b74f0c043f83fcb to your computer and use it in GitHub Desktop.
Save blixt/08434b74f0c043f83fcb to your computer and use it in GitHub Desktop.
A simple Event class for handling events in Swift without strong retain cycles.
import Foundation
private class Invoker<EventData> {
weak var listener: AnyObject?
let closure: (EventData) -> Bool
init<Listener : AnyObject>(listener: Listener, method: (Listener) -> (EventData) -> Void) {
self.listener = listener
self.closure = {
[weak listener] (data: EventData) in
guard let listener = listener else {
return false
}
method(listener)(data)
return true
}
}
}
class Event<EventData> {
private var invokers = [Invoker<EventData>]()
/// Adds an event listener, notifying the provided method when the event is emitted.
func addListener<Listener : AnyObject>(listener: Listener, method: (Listener) -> (EventData) -> Void) {
invokers.append(Invoker(listener: listener, method: method))
}
/// Removes the object from the list of objects that get notified of the event.
func removeListener(listener: AnyObject) {
invokers = invokers.filter {
guard let current = $0.listener else {
return false
}
return current !== listener
}
}
/// Publishes the specified data to all listeners via the global utility dispatch queue.
func emit(data: EventData) {
let queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)
for invoker in invokers {
dispatch_async(queue) {
// TODO: If this returns false, we should remove the invoker from the list.
invoker.closure(data)
}
}
}
}
// MARK: - Demo
class Player {
// A simple tuple to demonstrate multi-value events (could also be a struct/class of course).
typealias TrackData = (file: String, duration: Int)
// An event with no additional information.
let paused = Event<Void>()
// Another event that emits TrackData.
let trackChanged = Event<TrackData>()
// Demo function to show an event being emitted.
func emitStuff() {
// Emit the track changed event to all listeners.
trackChanged.emit((file: "song1.mp3", duration: 123))
// No need to pass in data if the event data type is Void.
paused.emit()
}
}
class MyClass {
let name: String
init(name: String) {
self.name = name
}
deinit {
print("Releasing \(name)!")
}
// Events with event type Void don't require any argument in the handler.
func handlePause() {
print("Handling pause in \(name)!")
}
// Note that tuples can be expanded in the handler signature, which makes the code cleaner.
func handleTrackChanged(file: String, duration: Int) {
print("Handling track changed to \(file) (\(duration) sec) in \(name)!")
}
}
let player = Player()
// Adding event handlers to different objects is simple.
let c1: MyClass = MyClass(name: "One")
player.trackChanged.addListener(c1, method: MyClass.handleTrackChanged)
let c2: MyClass = MyClass(name: "Two")
player.paused.addListener(c2, method: MyClass.handlePause)
player.trackChanged.addListener(c2, method: MyClass.handleTrackChanged)
// This is how you explicitly unregister an object from handling an event.
player.trackChanged.removeListener(c2)
// Objects that are not retained elsewhere will automatically be unregistered for events.
func scope() {
// c3 will be released at the end of this scope, so its handlePause method will never be called.
let c3: MyClass = MyClass(name: "Three")
player.paused.addListener(c3, method: MyClass.handlePause)
}
scope()
// Emit the events to all the listeners.
player.emitStuff()
@petebarber
Copy link

In the Invoker class why do you bother with the weak var listener: AnyObject? & the self.listener = listener. Isn't the weak capture into the closure sufficient?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment