Skip to content

Commit 8e7f43f

Browse files
committed
Decorator for explicitly-typed Event callbacks
- Implemented a decorator method called `callTypedEventCallback` to type-test and cast an `Eventable` to an explicit Event Type - Updated README.MD to reflect the above change.
1 parent 57d6d9f commit 8e7f43f

File tree

3 files changed

+75
-30
lines changed

3 files changed

+75
-30
lines changed

README.md

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -193,35 +193,31 @@ So, we have an *Event* type, and we are able to *Dispatch* it through a *Queue*
193193
class TemperatureProcessor: EventReceiver {
194194
/// Register our Event Listeners for this EventReceiver
195195
override func registerEventListeners() {
196-
addEventCallback(onTemperatureEvent, forEventType: TemperatureEvent.self)
196+
addEventCallback({ event, priority in
197+
self.callTypedEventCallback(self.onTemperatureEvent, forEvent: event, priority: priority)
198+
}, forEventType: TemperatureEvent.self)
197199
}
198200

199201
/// Define our Callback Function to process received TemperatureEvent Events
200-
func onTemperatureEvent(_ event: TestEventTypeOne, _ priority: EventPriority) {
201-
/// At the moment, it is necessary to type check and cast the Event ourselves...
202-
if let typedEvent = event as? TemperatureEvent { // If the Event conforms to TemperatureEvent...
203-
processTemperatureEvent(typedEvent) // ... invoke our concrete method to process the Event data.
204-
}
205-
}
206-
207-
/// Method to process our TemperatureEvent
208-
func processTemperatureEvent(_ event: TemperatureEvent) {
209-
202+
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority) {
203+
210204
}
211205
}
212206
```
213-
Before we dig into the implementation of `processTemperatureEvent`, which can basically do whatever we would want to do with the data provided in the `TemperatureEvent`, let's take a moment to understand what is happening in the above code.
207+
Before we dig into the implementation of `onTemperatureEvent`, which can basically do whatever we would want to do with the data provided in the `TemperatureEvent`, let's take a moment to understand what is happening in the above code.
214208

215209
Firstly, `TemperatureProcessor` inherits from `EventReceiver`, which is where all of the magic happens to receive *Events* and register our *Listeners* (or *Callbacks* or *Handlers*).
216210

217211
The function `registerEventListeners` will be called automatically when an instance of `TemperatureProcessor` is created. Within this method, we cal `addEventCallback` to register `onTemperatureEvent` so that it will be invoked every time an *Event* of type `TemperatureEvent` is *Dispatched*.
218212

219-
Our *Callback* (or *Handler* or *Listener Event*) is called `onTemperatureEvent`, and all this does is first check that the `event` received is of the type `TemperatureEvent`, and - if it is - passes the now-typed `TemperatureEvent` along to `processTemperatureEvent`, which is where we will implement whatever *Operation* is to be performed against a `TemperatureEvent`.
213+
Notice that we use a *Closure* which invokes `self.callTypedEventCallback`. This is to address a fundamental limitation of Generics in the Swift language, and acts as a decorator to perform the Type Checking and Casting of the received `event` to the explicit *Event* type we expect. In this case, that is `TemperatureEvent`
214+
215+
Our *Callback* (or *Handler* or *Listener Event*) is called `onTemperatureEvent`, which is where we will implement whatever *Operation* is to be performed against a `TemperatureEvent`.
220216

221217
**Note**: The need to provide type checking and casting (in `onTemperatureEvent`) is intended to be a temporary requirement. We are looking at ways to decorate this internally within the library, so that we can reduce the amount of boilerplate code you have to produce in your implementations.
222218
For the moment, this solution works well, and enables you to begin using `EventDrivenSwift` in your applications immediately.
223219

224-
Now, let's actually do something with our `TemperatureEvent` in the `processTemperatureEvent` method.
220+
Now, let's actually do something with our `TemperatureEvent` in the `onTemperatureEvent` method.
225221
```swift
226222
/// An Enum to map a Temperature value onto a Rating
227223
enum TemperatureRating: String {
@@ -253,13 +249,13 @@ Now, let's actually do something with our `TemperatureEvent` in the `processTemp
253249
@ThreadSafeSemaphore public var temperatureInCelsius: Float = Float.zero
254250
@ThreadSafeSemaphore public var temperatureRating: TemperatureRating = .freezing
255251

256-
func processTemperatureEvent(_ event: TemperatureEvent) {
252+
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority) {
257253
temperatureInCelsius = event.temperatureInCelsius
258254
temperatureRating = TemperatureRating.fromTemperature(event.temperatureInCelsius)
259255
}
260256
}
261257
```
262-
The above code is intended to be illustrative, rather than *useful*. Our `processTemperatureEvent` passes *Event*'s encapsulated `temperatureInCelsius` to a public variable (which could then be read by other code as necessary) as part of our `EventReceiver`, and also pre-calculates a `TemperatureRating` based on the Temperature value received in the *Event*.
258+
The above code is intended to be illustrative, rather than *useful*. Our `onTemperatureEvent` passes *Event*'s encapsulated `temperatureInCelsius` to a public variable (which could then be read by other code as necessary) as part of our `EventReceiver`, and also pre-calculates a `TemperatureRating` based on the Temperature value received in the *Event*.
263259

264260
Ultimately, your code can do whatever you wish with the *Event*'s *Payload* data!
265261

@@ -304,9 +300,9 @@ protocol TemperatureProcessorObserver: AnyObject {
304300
func onTemperatureEvent(temperatureInCelsius: Float)
305301
}
306302
```
307-
Now let's modify the `processTemperatureEvent` method we implemented in the previous example:
303+
Now let's modify the `onTemperatureEvent` method we implemented in the previous example:
308304
```swift
309-
func processTemperatureEvent(_ event: TemperatureEvent) {
305+
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority) {
310306
temperatureInCelsius = event.temperatureInCelsius
311307
temperatureRating = TemperatureRating.fromTemperature(event.temperatureInCelsius)
312308

@@ -332,9 +328,9 @@ enum TemperatureRatingEvent: Eventable {
332328
var temperatureRating: TemperatureRating
333329
}
334330
```
335-
With the *Event* type defined, we can now once more expand our `processTemperatureEvent` to *Dispatch* our reciprocal `TemperatureRatingEvent`:
331+
With the *Event* type defined, we can now once more expand our `onTemperatureEvent` to *Dispatch* our reciprocal `TemperatureRatingEvent`:
336332
```swift
337-
func processTemperatureEvent(_ event: TemperatureEvent) {
333+
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority) {
338334
temperatureInCelsius = event.temperatureInCelsius
339335
temperatureRating = TemperatureRating.fromTemperature(event.temperatureInCelsius)
340336

Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,30 @@ import ThreadSafeSwift
1111
import Observable
1212

1313
public class EventReceiver: EventHandler, EventReceivable {
14+
/**
15+
Convienience `typealias` used for Event Callbacks
16+
- Author: Simon J. Stuart
17+
- Version: 1.0.0
18+
*/
1419
typealias EventCallback = (_ event: any Eventable, _ priority: EventPriority) -> ()
1520

21+
/**
22+
Convienience `typealias` used for Typed Event Callbacks
23+
- Author: Simon J. Stuart
24+
- Version: 1.0.0
25+
*/
26+
typealias TypedEventCallback<TEvent: Any> = (_ event: TEvent, _ priority: EventPriority) -> ()
27+
28+
/**
29+
Map of `Eventable` qualified Type Names against `EventCallback` methods.
30+
- Author: Simon J. Stuart
31+
- Version: 1.0.0
32+
- Note: We use the Qualified Type Name as the Key because Types are not Hashable in Swift
33+
*/
1634
@ThreadSafeSemaphore private var eventCallbacks = [String:EventCallback]() //TODO: Make this a Revolving Door collection!
1735

36+
// @ThreadSafeSemaphore private var typedEventCallbacks = [String:Any]() //TODO: Find an implementation that works for strong-typed Event Callbacks (P.S. limitations of Swift Generics are very annoying!)
37+
1838
/**
1939
Invoke the appropriate Callback for the given Event
2040
- Author: Simon J. Stuart
@@ -23,16 +43,24 @@ public class EventReceiver: EventHandler, EventReceivable {
2343
override internal func processEvent(_ event: any Eventable, dispatchMethod: EventDispatchMethod, priority: EventPriority) {
2444
let eventTypeName = String(reflecting: type(of: event))
2545
var callback: EventCallback? = nil
26-
46+
2747
_eventCallbacks.withLock { eventCallbacks in
2848
callback = eventCallbacks[eventTypeName]
2949
}
30-
50+
3151
if callback == nil { return } // If there is no Callback, we will just return!
32-
52+
3353
callback!(event, priority)
3454
}
3555

56+
/**
57+
Registers an Event Callback for the given `Eventable` Type
58+
- Author: Simon J. Stuart
59+
- Version: 1.0.0
60+
- Parameters:
61+
- callback: The code to invoke for the given `Eventable` Type
62+
- forEventType: The `Eventable` Type for which to Register the Callback
63+
*/
3664
internal func addEventCallback(_ callback: @escaping EventCallback, forEventType: Eventable.Type) {
3765
let eventTypeName = String(reflecting: forEventType)
3866

@@ -44,6 +72,31 @@ public class EventReceiver: EventHandler, EventReceivable {
4472
EventCentral.shared.addListener(self, forEventType: forEventType)
4573
}
4674

75+
internal func callTypedEventCallback<TEvent: Eventable>(_ callback: @escaping TypedEventCallback<TEvent>, forEvent: Eventable, priority: EventPriority) {
76+
if let typedEvent = forEvent as? TEvent {
77+
callback(typedEvent, priority)
78+
}
79+
}
80+
81+
//TODO: Find an implementation that works for strong-typed Event Callbacks (P.S. limitations of Swift Generics are very annoying!)
82+
// internal func addEventCallback<TEvent: Eventable>(_ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type) {
83+
// let eventTypeName = String(reflecting: forEventType)
84+
//
85+
// _typedEventCallbacks.withLock { typedEventCallbacks in
86+
// typedEventCallbacks[eventTypeName] = callback
87+
// }
88+
//
89+
// /// We automatically register the Listener with the Central Event Dispatcher
90+
// EventCentral.shared.addListener(self, forEventType: forEventType)
91+
// }
92+
93+
/**
94+
Removes an Event Callback for the given `Eventable` Type
95+
- Author: Simon J. Stuart
96+
- Version: 1.0.0
97+
- Parameters:
98+
- forEventType: The `Eventable` Type for which to Remove the Callback
99+
*/
47100
internal func removeEventCallback(forEventType: any Eventable) {
48101
let eventTypeName = String(reflecting: forEventType)
49102

Tests/EventDrivenSwiftTests/BasicTests.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,15 @@ final class BasicTests: XCTestCase {
1919
@ThreadSafeSemaphore var foo: Int = 0
2020
public var awaiter = DispatchSemaphore(value: 0)
2121

22-
internal func eventOneCallback(_ event: any Eventable, _ priority: EventPriority) {
23-
if let eventOne = event as? TestEventTypeOne {
24-
eventOneCallback(eventOne, priority)
25-
}
26-
}
27-
2822
internal func eventOneCallback(_ event: TestEventTypeOne, _ priority: EventPriority) {
2923
foo = event.foo
3024
awaiter.signal()
3125
}
3226

3327
override func registerEventListeners() {
34-
addEventCallback(eventOneCallback, forEventType: TestEventTypeOne.self)
28+
addEventCallback({ event, priority in
29+
self.callTypedEventCallback(self.eventOneCallback, forEvent: event, priority: priority)
30+
}, forEventType: TestEventTypeOne.self)
3531
}
3632
}
3733

0 commit comments

Comments
 (0)