Skip to content

Commit 2399ec9

Browse files
committed
EventListener completed
- `EventListener` implementation completed - `EventCentral` updated to provide a centralised `EventListener` for global use - `Eventable` introduces two new methods: - `addListener` registers an arbitrary Listener against the referenced `Eventable` Type and returns a `UUID` token for that Listener - `removeListener` unregisters the Listener specified by its `UUID` token
1 parent b21ff99 commit 2399ec9

File tree

10 files changed

+245
-147
lines changed

10 files changed

+245
-147
lines changed

README.md

+49-3
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ let package = Package(
125125
dependencies: [
126126
.package(
127127
url: "https://github.com/Flowduino/EventDrivenSwift.git",
128-
.upToNextMajor(from: "2.0.0")
128+
.upToNextMajor(from: "3.0.0")
129129
),
130130
],
131131
//...
@@ -346,13 +346,59 @@ As you can see, we can create and *Dispatch* an *Event* in a single operation. T
346346
Now that we've walked through these basic Usage Examples, see if you can produce your own `EventReceiver` to process `TemperatureRatingEvent`s. Everything you need to achieve this has already been demonstrated in this document.
347347

348348
## `UIEventReceiver`
349-
Version 2.0.0 introduces the `UIEventReceiver` base class, which operates exactly the same way as `EventReciever`, with the notable difference being that your registered *Event* Callbacks will **always** be invoked on the `MainActor` (or "UI Thread"). You can simply inherit from `UIEventReceiver` instead of `EventReceiver` whenever it is imperative for one or more *Event* Callbacks to execute on the `MainActor`.
349+
Version 2.0.0 introduced the `UIEventReceiver` base class, which operates exactly the same way as `EventReciever`, with the notable difference being that your registered *Event* Callbacks will **always** be invoked on the `MainActor` (or "UI Thread"). You can simply inherit from `UIEventReceiver` instead of `EventReceiver` whenever it is imperative for one or more *Event* Callbacks to execute on the `MainActor`.
350+
351+
## `EventListener`
352+
Version 3.0.0 introduced the `EventListener` concept to the Library.
353+
354+
An `EventListener` is a universal way of subscribing to *Events*, anywhere in your code!
355+
356+
By design, `EventDrivenSwift` provides a *Central Event Listener*, which is automatically initialized should any of your code register a *Listener* for an *Event* by reference to the `Eventable` type.
357+
358+
**Important Note:** `EventListener` will always invoke the associated `Callbacks` on the same Thread (or `DispatchQueue`) from whence the *Listener* registered! This is an extremely useful behaviour, because it means that *Listeners* registered from the `MainActor` (or "UI Thread") will always execute on that Thread, with no additional overhead or code required by you.
359+
360+
Let's register a simple *Listener* in some arbitrary `class`. For this example, let's produce a hypothetical *View Model* that will *Listen* for `TemperatureRatingEvent`, and would invalidate an owning `View` to show the newly-received values.
361+
362+
For the sake of this example, let's define this the pure SwiftUI way, *without* taking advantage of our `Observable` library:
363+
```swift
364+
class TemperatureRatingViewModel: ObservableObject {
365+
@Published var temperatureInCelsius: Float
366+
@Published var temperatureRating: TemperatureRating
367+
368+
var listenerToken: UUID
369+
370+
internal func onTemperatureRatingEvent(_ event: TemperatureRatingEvent, _ priority: EventPriority) {
371+
temperatureInCelsius = event.temperatureInCelsius
372+
temperatureRating = event.temperatureRating
373+
}
374+
375+
init() {
376+
// Let's register our Event Listener Callback!
377+
listenerToken = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent)
378+
}
379+
}
380+
```
381+
It really is **that** simple!
382+
383+
We can use a direct reference to an `Eventable` type, and invoke the `addListener` method, automatically bound to all `Eventable` types, to register our *Listener*.
384+
385+
In the above example, whenever the *Reciprocal Event* named `TemperatureRatingEvent` is dispatched, the `onTemperatureRatingEvent` method of any `TemperatureRatingViewModel` instance(s) will be invoked, in the context of that *Event*!
386+
387+
Don't worry about managing the lifetime of your *Listener*! If the object which owns the *Listener* is destroyed, the *Listener* will be automatically unregistered for you!
388+
389+
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:
390+
```swift
391+
TemperatureRatingEvent.removeListener(listenerToken)
392+
```
393+
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.
394+
395+
`EventListener`s are an extremely versatile and very powerful addition to `EventDrivenSwift`.
350396

351397
## Features Coming Soon
352398
`EventDrivenSwift` is an evolving and ever-improving Library, so here is a list of the features you can expect in future releases:
353399
- **Event Pools** - A superset expanding upon a given `EventReceiver` descendant type to provide pooled processing based on given scaling rules and conditions.
354400

355-
These are the features intended for the next Release, which will either be *2.1.0* or *3.0.0* depending on whether these additions require interface-breaking changes to the interfaces in version *2.0.0*.
401+
These are the features intended for the next Release, which will either be *3.1.0* or *4.0.0* depending on whether these additions require interface-breaking changes to the interfaces in version *3.0.0*.
356402

357403
## License
358404

Sources/EventDrivenSwift/Central/EventCentral.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,15 @@ final public class EventCentral: EventDispatcher, EventCentralable {
6666
}
6767
}
6868

69-
internal var eventListener = EventListener()
69+
private var _eventListener: EventListenable?
70+
internal var eventListener: EventListenable {
71+
get {
72+
if _eventListener == nil { _eventListener = EventListener() }
73+
return _eventListener!
74+
}
75+
}
7076

71-
@inline(__always) public static func addListener<TEvent>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type) -> UUID where TEvent : Eventable {
77+
@discardableResult @inline(__always) public static func addListener<TEvent>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type) -> UUID where TEvent : Eventable {
7278
return _shared.eventListener.addListener(requester, callback, forEventType: forEventType)
7379
}
7480

Sources/EventDrivenSwift/Central/EventCentralable.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public protocol EventCentralable {
6868
- forEventType: The `Eventable` Type for which to Register the Callback
6969
- Returns: A `UUID` value representing the `token` associated with this Event Callback
7070
*/
71-
static func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type) -> UUID
71+
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type) -> UUID
7272

7373
/**
7474
Locates and removes the given Listener `token` (if it exists) from the Central Event Listener

Sources/EventDrivenSwift/Event/Eventable.swift

+30-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,26 @@ public protocol Eventable {
3333
- priority: The Priority with which to process this Event
3434
*/
3535
func stack(priority: EventPriority)
36+
37+
/**
38+
Registers an Event Listner Callback for the given `Eventable` Type with the Central Event Listener
39+
- Author: Simon J. Stuart
40+
- Version: 3.0.0
41+
- Parameters:
42+
- requester: The Object owning the Callback Method
43+
- callback: The code to invoke for the given `Eventable` Type
44+
- Returns: A `UUID` value representing the `token` associated with this Event Callback
45+
*/
46+
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>) -> UUID
47+
48+
/**
49+
Locates and removes the given Listener `token` (if it exists) from the Central Event Listener
50+
- Author: Simon J. Stuart
51+
- Version: 3.0.0
52+
- Parameters:
53+
- token: The Token of the Listener you wish to remove
54+
*/
55+
static func removeListener(_ token: UUID)
3656
}
3757

3858
/**
@@ -53,10 +73,18 @@ extension Eventable {
5373
*/
5474
extension Eventable {
5575
public func queue(priority: EventPriority = .normal) {
56-
EventCentral.shared.queueEvent(self, priority: priority)
76+
EventCentral.queueEvent(self, priority: priority)
5777
}
5878

5979
public func stack(priority: EventPriority = .normal) {
60-
EventCentral.shared.stackEvent(self, priority: priority)
80+
EventCentral.stackEvent(self, priority: priority)
81+
}
82+
83+
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>) -> UUID {
84+
return EventCentral.addListener(requester, callback, forEventType: Self.self)
85+
}
86+
87+
static func removeListener(_ token: UUID) {
88+
EventCentral.removeListener(token, typeOf: Self.self)
6189
}
6290
}

Sources/EventDrivenSwift/EventListener/EventListenable.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public protocol EventListenable: AnyObject, EventReceivable {
3838
- forEventType: The `Eventable` Type for which to Register the Callback
3939
- Returns: A `UUID` value representing the `token` associated with this Event Callback
4040
*/
41-
func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type) -> UUID
41+
@discardableResult func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type) -> UUID
4242

4343
/**
4444
Locates and removes the given Listener `token` (if it exists)

Sources/EventDrivenSwift/EventListener/EventListener.swift

+1-10
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ open class EventListener: EventHandler, EventListenable {
6969
}
7070
}
7171

72-
public func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type) -> UUID {
72+
@discardableResult public func addListener<TEvent: Eventable>(_ requester: AnyObject, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type) -> UUID {
7373
let eventTypeName = String(reflecting: forEventType)
7474
let method: EventCallback = { event, priority in
7575
self.callTypedEventCallback(callback, forEvent: event, priority: priority)
@@ -145,13 +145,4 @@ open class EventListener: EventHandler, EventListenable {
145145
callback(typedEvent, priority)
146146
}
147147
}
148-
149-
/**
150-
Initializes an `EventListener`
151-
- Author: Simon J. Stuart
152-
- Version: 3.0.0
153-
*/
154-
public override init() {
155-
super.init()
156-
}
157148
}

Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift

-14
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,6 @@ import Observable
1717
- Note: Inherit from this to implement a discrete unit of code designed specifically to operate upon specific `Eventable` types containing information useful to its operation(s)
1818
*/
1919
open class EventReceiver: EventHandler, EventReceivable {
20-
/**
21-
Convienience `typealias` used for Event Callbacks
22-
- Author: Simon J. Stuart
23-
- Version: 1.0.0
24-
*/
25-
typealias EventCallback = (_ event: any Eventable, _ priority: EventPriority) -> ()
26-
27-
/**
28-
Convienience `typealias` used for Typed Event Callbacks
29-
- Author: Simon J. Stuart
30-
- Version: 1.0.0
31-
*/
32-
typealias TypedEventCallback<TEvent: Any> = (_ event: TEvent, _ priority: EventPriority) -> ()
33-
3420
/**
3521
Map of `Eventable` qualified Type Names against `EventCallback` methods.
3622
- Author: Simon J. Stuart
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// EventListenerTests.swift
3+
//
4+
//
5+
// Created by Simon Stuart on 11/08/2022.
6+
//
7+
8+
import XCTest
9+
import ThreadSafeSwift
10+
@testable import EventDrivenSwift
11+
12+
final class EventListenerTests: XCTestCase {
13+
struct TestEventTypeOne: Eventable {
14+
var foo: Int
15+
}
16+
let expectedTestOneFoo: Int = 1000
17+
var myFoo = 0
18+
var awaiter = DispatchSemaphore(value: 0)
19+
20+
/// Need a reliable way of Unit Testing this! Anything I do to try to await a result causes this Execution Thread to lock, so the Callback won't occur until *after* the test returns!
21+
22+
// func testCentralEventListener() throws {
23+
// let listenerToken = TestEventTypeOne.addListener(self) { (event: TestEventTypeOne, priority) in
24+
// self.myFoo = event.foo
25+
// self.awaiter.signal()
26+
// }
27+
// // Make sure our Initial Value is as expected
28+
// XCTAssertEqual(myFoo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(myFoo)")
29+
// let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event
30+
// testOne.queue() // Dispatch the Event
31+
//
32+
// let result = awaiter.wait(timeout: DispatchTime.now().advanced(by: DispatchTimeInterval.seconds(10)))
33+
//
34+
// XCTAssertEqual(result, .success, "The Event Handler was not invoked in time!")
35+
//
36+
// XCTAssertEqual(self.myFoo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(self.myFoo)")
37+
// TestEventTypeOne.removeListener(listenerToken)
38+
// }
39+
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// UIEventReceiverTests.swift
3+
//
4+
//
5+
// Created by Simon Stuart on 11/08/2022.
6+
//
7+
8+
import XCTest
9+
import ThreadSafeSwift
10+
@testable import EventDrivenSwift
11+
12+
final class UIEventReceiverTests: XCTestCase {
13+
struct TestEventTypeOne: Eventable {
14+
var foo: Int
15+
}
16+
17+
class TestEventThread: UIEventReceiver {
18+
@ThreadSafeSemaphore var foo: Int = 0
19+
20+
internal func eventOneCallback(_ event: TestEventTypeOne, _ priority: EventPriority) {
21+
foo = event.foo
22+
}
23+
24+
override func registerEventListeners() {
25+
addEventCallback(self.eventOneCallback, forEventType: TestEventTypeOne.self)
26+
}
27+
}
28+
29+
let expectedTestOneFoo: Int = 1000
30+
31+
/// Need a reliable way of Unit Testing this! Anything I do to try to await a result causes this Execution Thread to lock, so the Callback won't occur until *after* the test returns!
32+
33+
// func testEventDispatchQueueDirect() throws {
34+
// let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event
35+
// let eventThread = TestEventThread() // Create the Thread
36+
//
37+
// XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)")
38+
//
39+
// eventThread.queueEvent(testOne, priority: .normal) // Now let's dispatch our Event to change this value
40+
//
41+
// let seconds = 1.0
42+
// DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
43+
// XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)")
44+
// }
45+
// }
46+
//
47+
// func testEventDispatchQueueCentral() throws {
48+
// let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event
49+
// let eventThread = TestEventThread() // Create the Thread
50+
//
51+
// XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)")
52+
// EventCentral.queueEvent(testOne, priority: .normal) // Now let's dispatch our Event to change this value
53+
//
54+
// let seconds = 1.0
55+
// DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
56+
// XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)")
57+
// }
58+
// }
59+
//
60+
// func testEventDispatchQueueTransparent() throws {
61+
// let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event
62+
// let eventThread = TestEventThread() // Create the Thread
63+
//
64+
// XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)")
65+
//
66+
// testOne.queue() // Now let's dispatch our Event to change this value
67+
//
68+
// let seconds = 1.0
69+
// DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
70+
// XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)")
71+
// }
72+
// }
73+
//
74+
// func testEventDispatchStackDirect() throws {
75+
// let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event
76+
// let eventThread = TestEventThread() // Create the Thread
77+
//
78+
// XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)")
79+
//
80+
// eventThread.stackEvent(testOne, priority: .normal) // Now let's dispatch our Event to change this value
81+
//
82+
// let seconds = 1.0
83+
// DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
84+
// XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)")
85+
// }
86+
// }
87+
//
88+
// func testEventDispatchStackCentral() throws {
89+
// let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event
90+
// let eventThread = TestEventThread() // Create the Thread
91+
//
92+
// XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)")
93+
//
94+
// EventCentral.stackEvent(testOne, priority: .normal) // Now let's dispatch our Event to change this value
95+
//
96+
// let seconds = 1.0
97+
// DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
98+
// XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)")
99+
// }
100+
// }
101+
//
102+
// func testEventDispatchStackTransparent() throws {
103+
// let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event
104+
// let eventThread = TestEventThread() // Create the Thread
105+
//
106+
// XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)")
107+
//
108+
// testOne.stack() // Now let's dispatch our Event to change this value
109+
//
110+
// let seconds = 1.0
111+
// DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
112+
// XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)")
113+
// }
114+
// }
115+
}

0 commit comments

Comments
 (0)