Skip to content

Better event callbacks #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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