Skip to content

Commit

Permalink
Merge pull request #3 from Flowduino/better-event-callbacks
Browse files Browse the repository at this point in the history
Better event callbacks
  • Loading branch information
LK-Simon authored Aug 28, 2022
2 parents 05531f7 + 8fdcefc commit c1600f1
Show file tree
Hide file tree
Showing 15 changed files with 459 additions and 51 deletions.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ class TemperatureRatingViewModel: ObservableObject {
@Published var temperatureInCelsius: Float
@Published var temperatureRating: TemperatureRating

var listenerToken: UUID
var listenerHandle: EventListenerHandling?

internal func onTemperatureRatingEvent(_ event: TemperatureRatingEvent, _ priority: EventPriority) {
temperatureInCelsius = event.temperatureInCelsius
Expand All @@ -379,7 +379,7 @@ class TemperatureRatingViewModel: ObservableObject {

init() {
// Let's register our Event Listener Callback!
listenerToken = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent)
listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent)
}
}
```
Expand All @@ -393,22 +393,24 @@ Don't worry about managing the lifetime of your *Listener*! If the object which

If you need your *Event Callback* to execute on the *Listener's* Thread, as of Version 3.1.0... you can!
```swift
listenerToken = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, executeOn: .listenerThread)
listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, executeOn: .listenerThread)
```
**Remember:** When executing an *Event Callback* on `.listenerThread`, you will need to ensure that your *Callback* and all resources that it uses are 100% Thread-Safe!
**Important:** Executing the *Event Callback* on `.listnerThread` can potentially delay the invocation of other *Event Callbacks*. Only use this option when it is necessary.

You can also execute your *Event Callback* on an ad-hoc `Task`:
```swift
listenerToken = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, executeOn: .taskThread)
listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, executeOn: .taskThread)
```
**Remember:** When executing an *Event Callback* on `.taskThread`, you will need to ensure that your *Callback* and all resources that it uses are 100% Thread-Safe!

Another thing to note about the above example is the `listenerToken`. Whenever you register a *Listener*, it will return a Unique Universal ID (a `UUID`) value. You can use this value to *Unregister* your *Listener* at any time:
Another thing to note about the above example is the `listenerHandle`. Whenever you register a *Listener*, it will return an `EventListenerHandling` object. You can use this value to *Unregister* your *Listener* at any time:
```swift
TemperatureRatingEvent.removeListener(listenerToken)
listenerHandle.remove()
```
This way, when an *Event* is no longer relevant to your code, you can simply call `removeListener` against the `Eventable` type, and pass in the token returned when you added the *Listener* in the first place.
This will remove your *Listener Callback*, meaning it will no longer be invoked any time a `TemperatureRatingEvent` is *Dispatched*.

**Note:** This is an improvement for Version 4.1.0, as opposed to the use of an untyped `UUID` from previous versions.

`EventListener`s are an extremely versatile and very powerful addition to `EventDrivenSwift`.

Expand Down
2 changes: 1 addition & 1 deletion Sources/EventDrivenSwift/Central/EventCentral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ final public class EventCentral: EventDispatcher, EventCentralable {
}
}

@discardableResult @inline(__always) public static func addListener<TEvent>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread) -> UUID where TEvent : Eventable {
@discardableResult @inline(__always) public static func addListener<TEvent>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread) -> EventListenerHandling where TEvent : Eventable {
return _shared.eventListener.addListener(requester, callback, forEventType: forEventType, executeOn: executeOn)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/EventDrivenSwift/Central/EventCentralable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public protocol EventCentralable {
- forEventType: The `Eventable` Type for which to Register the Callback
- Returns: A `UUID` value representing the `token` associated with this Event Callback
*/
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn) -> UUID
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn) -> EventListenerHandling

/**
Locates and removes the given Listener `token` (if it exists) from the Central Event Listener
Expand Down
9 changes: 6 additions & 3 deletions Sources/EventDrivenSwift/Event/Eventable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ public protocol Eventable {
- callback: The code to invoke for the given `Eventable` Type
- Returns: A `UUID` value representing the `token` associated with this Event Callback
*/
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn) -> UUID
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn) -> EventListenerHandling

// @discardableResult static func addListener(_ requester: AnyObject?, _ eventType: any Eventable.Type, _ callback: @escaping TypedEventCallback<any Eventable.Type>, executeOn: ExecuteEventOn) -> UUID

/**
Locates and removes the given Listener `token` (if it exists) from the Central Event Listener
Expand Down Expand Up @@ -80,11 +82,12 @@ extension Eventable {
EventCentral.stackEvent(self, priority: priority)
}

@discardableResult static public func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn = .requesterThread) -> UUID {
@discardableResult static public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn = .requesterThread) -> EventListenerHandling {
return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn)
}

public static func removeListener(_ token: UUID) {
EventCentral.removeListener(token, typeOf: Self.self)
}
}

60 changes: 60 additions & 0 deletions Sources/EventDrivenSwift/Event/Wrappers/EventMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// EventMethod.swift
// Copyright (c) 2022, Flowduino
// Authored by Simon J. Stuart on 21st August 2022
//
// Subject to terms, restrictions, and liability waiver of the MIT License
//

import Foundation

public typealias EventMethodTypedEventCallback<TOwner: AnyObject, TEvent: Any> = (_ sender: TOwner, _ event: TEvent, _ priority: EventPriority) -> ()

/**
Any Property wrapped with `EventMethod` will automatically conform to `EventMethodContainer`
- Author: Simon J. Stuart
- Version: 4.1.0
- Note: This is used to conformity-test decorated `var`s to automatically register Event Listeners
*/
public protocol EventMethodContainer {
associatedtype TEventType: Eventable
associatedtype TOwner: AnyObject

var wrappedValue: EventMethodTypedEventCallback<TOwner, TEventType>? { get set }

mutating func prepare(owner: AnyObject)
}

/**
Decorate Typed Event Callback Closures as `var` with `@EventMethod<TEventType>` to automatically register them.
- Author: Simon J. Stuart
- Version: 4.1.0
*/
@propertyWrapper
public struct EventMethod<TOwner: AnyObject, TEventType: Eventable>: EventMethodContainer {
public var wrappedValue: EventMethodTypedEventCallback<TOwner, TEventType>?
public var executeOn: ExecuteEventOn

private weak var owner: AnyObject? = nil

private func callback(event: TEventType, priority: EventPriority) {
if let typedOwner = owner as? TOwner {
wrappedValue?(typedOwner, event, priority)
}
}

public init(wrappedValue: EventMethodTypedEventCallback<TOwner, TEventType>?, executeOn: ExecuteEventOn = .requesterThread) {
self.wrappedValue = wrappedValue
self.executeOn = executeOn
}

mutating public func prepare(owner: AnyObject) {
if let typedOwner = owner as? TOwner {
self.owner = owner
TEventType.addListener(
typedOwner,
callback,
executeOn: executeOn)
}
}
}
6 changes: 3 additions & 3 deletions Sources/EventDrivenSwift/EventHandler/EventHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ open class EventHandler: ObservableThread, EventHandling {
var eventStacks = [EventPriority:[any Eventable]]()
_stacks.withLock { stacks in
eventStacks = stacks
stacks.removeAll(keepingCapacity: true)
stacks.removeAll()
}

for priority in EventPriority.inOrder {
Expand All @@ -147,7 +147,7 @@ open class EventHandler: ObservableThread, EventHandling {
var eventQueues = [EventPriority:[any Eventable]]()
_queues.withLock { queues in
eventQueues = queues
queues.removeAll(keepingCapacity: true)
queues.removeAll()
}

for priority in EventPriority.inOrder {
Expand All @@ -173,7 +173,7 @@ open class EventHandler: ObservableThread, EventHandling {
- Version: 1.0.0
*/
public override func main() {
while isExecuting {
while isExecuting && !isCancelled {
eventsPending.wait() // This will make the Thread effectively "sleep" until there are Events pending
processAllEvents() // Once there's at least one Event waiting, we will Process it/them.
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/EventDrivenSwift/EventListener/EventListenable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Foundation
- Author: Simon J. Stuart
- Version: 3.0.0
*/
typealias EventCallback = (_ event: any Eventable, _ priority: EventPriority) -> ()
public typealias EventCallback = (_ event: any Eventable, _ priority: EventPriority) -> ()

/**
Convienience `typealias` used for Typed Event Callbacks
Expand Down Expand Up @@ -61,7 +61,7 @@ public protocol EventListenable: AnyObject, EventReceiving {
- executeOn: Tells the `EventListenable` whether to execute the Callback on the `requester`'s Thread, or the Listener's.
- Returns: A `UUID` value representing the `token` associated with this Event Callback
*/
@discardableResult func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn) -> UUID
@discardableResult func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn) -> EventListenerHandling

/**
Locates and removes the given Listener `token` (if it exists)
Expand Down
4 changes: 2 additions & 2 deletions Sources/EventDrivenSwift/EventListener/EventListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ open class EventListener: EventHandler, EventListenable {
}
}

@discardableResult public func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread) -> UUID {
@discardableResult public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread) -> EventListenerHandling {
let eventTypeName = String(reflecting: forEventType)
let method: EventCallback = { event, priority in
self.callTypedEventCallback(callback, forEvent: event, priority: priority)
Expand All @@ -95,7 +95,7 @@ open class EventListener: EventHandler, EventListenable {

/// We automatically register the Listener with the Central Event Dispatcher
EventCentral.shared.addReceiver(self, forEventType: forEventType)
return eventListenerContainer.token
return EventListenerHandler(eventListenable: self, token: eventListenerContainer.token)
}

public func removeListener(_ token: UUID) {
Expand Down
47 changes: 47 additions & 0 deletions Sources/EventDrivenSwift/EventListener/EventListening.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// EventListening.swift
// Copyright (c) 2022, Flowduino
// Authored by Simon J. Stuart on 4th August 2022
//
// Subject to terms, restrictions, and liability waiver of the MIT License
//

import Foundation

/**
Apply `EventListening` to any `Class` intent on listening for `Eventable`s to register `@EventMethod`-decorated (immutable) Listeners via Reflection.
- Author: Simon J. Stuart
- Version: 4.1.0
*/
public protocol EventListening: AnyObject {
/**
Invoke this method to automatically register any Event Listener callback bearing the `@EventMethod` wrapper.
- Author: Simon J. Stuart
- Version: 4.1.0
- Note: Any Event Callback implemented this way will be automatically registered for you.
````
@EventMethod<MyEventThreadType, MyEventType>
private var onMyEvent = {
(self, event: MyEventType, priority: EventPriority) in
/// Do something with `MyEventType` via its `event` reference here
}
````
*/
func registerListeners()
}

/**
Universal implementations to automatically Register and Unregister `@EventMethod`-decorated Event Listener Callbacks using Reflection
- Author: Simon J. Stuart
- Version: 4.1.0
*/
public extension EventListening {
func registerListeners() {
let mirror = Mirror(reflecting: self)
for child in mirror.children {
if var child = child.value as? (any EventMethodContainer) {
child.prepare(owner: self)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// EventListenerHandler.swift
// Copyright (c) 2022, Flowduino
// Authored by Simon J. Stuart on 28th August 2022
//
// Subject to terms, restrictions, and liability waiver of the MIT License
//

import Foundation

/**
A Handler for an `EventListener` your code has registered. You can use this to revoke your Event Listeners at any time.
- Author: Simon J. Stuart
- Version: 4.1.0
*/
public class EventListenerHandler: EventListenerHandling {
/**
`weak` reference to the `EventListenable` against which this Listener is registered.
- Author: Simon J. Stuart
- Version: 4.1.0
*/
private weak var eventListenable: EventListenable?

/**
This is the Token Key assoicated with your Listener
- Author: Simon J. Stuart
- Version: 4.1.0
*/
private var token: UUID

public func remove() {
eventListenable?.removeListener(token)
}

public init(eventListenable: EventListenable, token: UUID) {
self.eventListenable = eventListenable
self.token = token
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// EventListenerHandling.swift
// Copyright (c) 2022, Flowduino
// Authored by Simon J. Stuart on 28th August 2022
//
// Subject to terms, restrictions, and liability waiver of the MIT License
//

import Foundation

/**
A Handler for an `EventListener` your code has registered. You can use this to revoke your Event Listeners at any time.
- Author: Simon J. Stuart
- Version: 4.1.0
*/
public protocol EventListenerHandling: AnyObject {
/**
Removes your Event Listener
*/
func remove()
}
2 changes: 1 addition & 1 deletion Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ import Observable
- Note: `EventThread` inherits from this
*/
open class EventReceiver: EventHandler, EventReceiving {

}
Loading

0 comments on commit c1600f1

Please sign in to comment.