Skip to content

Commit 951b5c8

Browse files
authored
Merge pull request #7 from Flowduino/custom-event-filtering
5.2.0 - Custom Event Filtering for `EventListener`
2 parents 48cdd12 + 1784643 commit 951b5c8

File tree

9 files changed

+104
-10
lines changed

9 files changed

+104
-10
lines changed

README.md

+31
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,37 @@ In the above code example, `maximumAge` is a value defined in *nanoseconds*. Wit
477477

478478
This functionality is very useful when the context of an *Event*'s usage would have a known, fixed expiry.
479479

480+
## `EventListener` with *Custom Event Filtering* Interest
481+
Version 5.2.0 of this library introduces the concept of *Custom Event Filtering* for *Listeners*.
482+
483+
Now, when registering a *Listener* for an `Eventable` type, you can specify a `customFilter` *Callback* which, ultimately, returns a `Bool` where `true` means that the *Listener* is interested in the *Event*, and `false` means that the *Listener* is **not** interested in the *Event*.
484+
485+
We have made it simple for you to configure a *Custom Filter* for your *Listener*. Taking the previous code example, we can simply modify it as follows:
486+
```swift
487+
class TemperatureRatingViewModel: ObservableObject {
488+
@Published var temperatureInCelsius: Float
489+
@Published var temperatureRating: TemperatureRating
490+
491+
var listenerHandle: EventListenerHandling?
492+
493+
internal func onTemperatureRatingEvent(_ event: TemperatureRatingEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {
494+
temperatureInCelsius = event.temperatureInCelsius
495+
temperatureRating = event.temperatureRating
496+
}
497+
498+
internal func onTemperatureRatingEventFilter(_ event: TemperatureRatingEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> Bool {
499+
if event.temperatureInCelsius > 50 { return false } // If the Temperature is above 50 Degrees, this Listener is not interested in it!
500+
return true // If the Temperature is NOT above 50 Degrees, the Listener IS interested in it!
501+
}
502+
503+
init() {
504+
// Let's register our Event Listener Callback!
505+
listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, interestedIn: .custom, customFilter: onTemperatureRatingEventFilter)
506+
}
507+
}
508+
```
509+
The above code will ensure that the `onTemperatureRatingEvent` method is only invoked for a `TemperatureRatingEvent` where its `temperatureInCelsius` is less than or equal to 50 Degrees Celsius. Any `TemperatureRatingEvent` with a `temperatureInCelsius` greater than 50 will simply be ignored by this *Listener*.
510+
480511
## `EventPool`
481512
Version 4.0.0 introduces the extremely powerful `EventPool` solution, making it possible to create managed groups of `EventThread`s, where inbound *Events* will be directed to the best `EventThread` in the `EventPool` at any given moment.
482513

Sources/EventDrivenSwift/Central/EventCentral.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ final public class EventCentral: EventDispatcher, EventCentralable {
7575
}
7676
}
7777

78-
@discardableResult @inline(__always) public static func addListener<TEvent>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0) -> EventListenerHandling where TEvent : Eventable {
79-
return _shared.eventListener.addListener(requester, callback, forEventType: forEventType, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge)
78+
@discardableResult @inline(__always) public static func addListener<TEvent>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0, customFilter: TypedEventFilterCallback<TEvent>? = nil) -> EventListenerHandling where TEvent : Eventable {
79+
return _shared.eventListener.addListener(requester, callback, forEventType: forEventType, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge, customFilter: customFilter)
8080
}
8181

8282
@inline(__always) public static func removeListener(_ token: UUID) {

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-
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling
71+
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64, customFilter: TypedEventFilterCallback<TEvent>?) -> EventListenerHandling
7272

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

Sources/EventDrivenSwift/Event/Eventable.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public protocol Eventable {
6363
- callback: The code to invoke for the given `Eventable` Type
6464
- Returns: A `UUID` value representing the `token` associated with this Event Callback
6565
*/
66-
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling
66+
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64, customFilter: TypedEventFilterCallback<TEvent>?) -> EventListenerHandling
6767

6868
/**
6969
Locates and removes the given Listener `token` (if it exists) from the Central Event Listener
@@ -122,8 +122,8 @@ extension Eventable {
122122
EventCentral.scheduleStack(self, at: at, priority: priority)
123123
}
124124

125-
@discardableResult static public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0) -> EventListenerHandling {
126-
return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge)
125+
@discardableResult static public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0, customFilter: TypedEventFilterCallback<TEvent>? = nil) -> EventListenerHandling {
126+
return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge, customFilter: customFilter)
127127
}
128128

129129
public static func removeListener(_ token: UUID) {

Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ open class EventDispatcher: EventHandler, EventDispatching {
5454

5555
_receivers.withLock { receivers in
5656
var bucket = receivers[eventTypeName]
57-
if bucket == nil { return } /// Can't remove a Receiver if there isn't even a Bucket for hte Event Type
57+
if bucket == nil { return } /// Can't remove a Receiver if there isn't even a Bucket for the Event Type
5858

5959
/// Remove any Receivers from this Event-Type Bucket for the given `receiver` instance.
6060
bucket!.removeAll { receiverContainer in

Sources/EventDrivenSwift/EventListener/EventListenable.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Foundation
1414
- Version: 3.0.0
1515
*/
1616
public typealias EventCallback = (_ event: any Eventable, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> ()
17+
public typealias EventFilterCallback = (_ event: any Eventable, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> Bool
1718

1819
/**
1920
Convienience `typealias` used for Typed Event Callbacks
@@ -63,7 +64,7 @@ public protocol EventListenable: AnyObject, EventReceiving {
6364
- maximumAge: If `interestedIn` == `.youngerThan`, this is the number of nanoseconds between the time of dispatch and the moment of processing where the Listener will be interested in the Event. Any Event older will be ignored
6465
- Returns: A `UUID` value representing the `token` associated with this Event Callback
6566
*/
66-
@discardableResult func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling
67+
@discardableResult func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64, customFilter: TypedEventFilterCallback<TEvent>?) -> EventListenerHandling
6768

6869
/**
6970
Locates and removes the given Listener `token` (if it exists)

Sources/EventDrivenSwift/EventListener/EventListener.swift

+29-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ open class EventListener: EventHandler, EventListenable {
3434
var executeOn: ExecuteEventOn = .requesterThread
3535
var interestedIn: EventListenerInterest = .all
3636
var maximumEventAge: UInt64 = 0
37+
var customFilter: EventFilterCallback?
3738
}
3839

3940
/**
@@ -73,6 +74,8 @@ open class EventListener: EventHandler, EventListenable {
7374

7475
if listener.interestedIn == .youngerThan && listener.maximumEventAge != 0 && (DispatchTime.now().uptimeNanoseconds - event.dispatchTime.uptimeNanoseconds) > listener.maximumEventAge { continue } // If this Receiver has a maximum age of interest, and this Event is older than that... skip it!
7576

77+
if listener.interestedIn == .custom && (listener.customFilter == nil || !listener.customFilter!(event.event, priority, event.dispatchTime)) { continue }
78+
7679
switch listener.executeOn {
7780
case .requesterThread:
7881
Task { // We raise a Task because we don't want the entire Listener blocked in the event the dispatchQueue is busy or blocked!
@@ -91,12 +94,18 @@ open class EventListener: EventHandler, EventListenable {
9194
}
9295
}
9396

94-
@discardableResult public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0) -> EventListenerHandling {
97+
@discardableResult public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0, customFilter: TypedEventFilterCallback<TEvent>? = nil) -> EventListenerHandling {
9598
let eventTypeName = forEventType.getEventTypeName()
9699
let method: EventCallback = { event, priority, dispatchTime in
97100
self.callTypedEventCallback(callback, forEvent: event, priority: priority, dispatchTime: dispatchTime)
98101
}
99-
let eventListenerContainer = EventListenerContainer(requester: requester, callback: method, dispatchQueue: OperationQueue.current?.underlyingQueue, executeOn: executeOn, interestedIn: interestedIn, maximumEventAge: maximumAge)
102+
var filterMethod: EventFilterCallback? = nil
103+
if customFilter != nil {
104+
filterMethod = { event, priority, dispatchTime in
105+
self.callTypedEventFilterCallback(customFilter!, forEvent: event, priority: priority, dispatchTime: dispatchTime)
106+
}
107+
}
108+
let eventListenerContainer = EventListenerContainer(requester: requester, callback: method, dispatchQueue: OperationQueue.current?.underlyingQueue, executeOn: executeOn, interestedIn: interestedIn, maximumEventAge: maximumAge, customFilter: filterMethod)
100109
_eventListeners.withLock { eventCallbacks in
101110
var bucket = eventCallbacks[eventTypeName]
102111
if bucket == nil { bucket = [EventListenerContainer]() } // Create a new bucket if there isn't already one!
@@ -161,10 +170,28 @@ open class EventListener: EventHandler, EventListenable {
161170
- callback: The code (Closure or Callback Method) to execute for the given `forEvent`, typed generically using `TEvent`
162171
- forEvent: The instance of the `Eventable` type to be processed
163172
- priority: The `EventPriority` with which the `forEvent` was dispatched
173+
- dispatchTime: The `DispatchTime` at which `forEvent` was Dispatched
164174
*/
165175
internal func callTypedEventCallback<TEvent: Eventable>(_ callback: @escaping TypedEventCallback<TEvent>, forEvent: Eventable, priority: EventPriority, dispatchTime: DispatchTime) {
166176
if let typedEvent = forEvent as? TEvent {
167177
callback(typedEvent, priority, dispatchTime)
168178
}
169179
}
180+
181+
/**
182+
Performs a Transparent Type Test, Type Cast, and Method Call to the Custom Filter via a `callback` Closure.
183+
- Author: Simon J. Stuart
184+
- Version: 5.2.0
185+
- Parameters:
186+
- callback: The code (Closure or Callback Method) to execute for the given `forEvent`, typed generically using `TEvent`... returns `true` if the Listener is interested in `forEvent`, `false` if the Listener wants to ignore it
187+
- forEvent: The instance of the `Eventable` type to be processed
188+
- priority: The `EventPriority` with which the `forEvent` was dispatched
189+
- dispatchTime: The `DispatchTime` at which `forEvent` was Dispatched
190+
*/
191+
internal func callTypedEventFilterCallback<TEvent: Eventable>(_ callback: @escaping TypedEventFilterCallback<TEvent>, forEvent: Eventable, priority: EventPriority, dispatchTime: DispatchTime) -> Bool {
192+
if let typedEvent = forEvent as? TEvent {
193+
return callback(typedEvent, priority, dispatchTime)
194+
}
195+
return false /// We will simply return `false` if the Event is of the wrong Type
196+
}
170197
}

Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift

+9
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,13 @@ public enum EventListenerInterest: CaseIterable {
2525
- Version: 5.0.0
2626
*/
2727
case youngerThan
28+
29+
/**
30+
Receivers will ignore any Event where the Filter Callback returns `false`, and accept any Event where the Filter Callback returns `true`
31+
- Author: Simon J. Stuart
32+
- Version: 5.2.0
33+
*/
34+
case custom
2835
}
36+
37+
public typealias TypedEventFilterCallback<EventType: Eventable> = (_ event: EventType, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> Bool

Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift

+26
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ final class BasicEventListenerTests: XCTestCase, EventListening {
3737
var myFoo = 0
3838
var listenerHandler: EventListenerHandling? = nil
3939
let testOne = TestEventTypeOne(foo: 1000) // Create the Event
40+
let testZero = TestEventTypeOne(foo: 2000) // Create the Event
4041
var awaiter = DispatchSemaphore(value: 0)
4142

4243
func testEventListenerOnListenerThread() throws {
@@ -76,4 +77,29 @@ final class BasicEventListenerTests: XCTestCase, EventListening {
7677

7778
listenerHandler?.remove()
7879
}
80+
81+
func testEventListenerCustomFilter() throws {
82+
XCTAssertEqual(myFoo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(myFoo)")
83+
84+
listenerHandler = TestEventTypeOne.addListener(self, { (event: TestEventTypeOne, priority, dispatchTime) in
85+
self.myFoo = event.foo
86+
self.awaiter.signal()
87+
}, executeOn: .taskThread, interestedIn: .custom, customFilter: { (event: TestEventTypeOne, priority, dispatchTime) in
88+
if event.foo == 1000 {
89+
print("Accepting Event because foo = 1000")
90+
return true
91+
}
92+
print("Ignoring Event where foo = \(event.foo)")
93+
return false
94+
})
95+
96+
testOne.queue()
97+
testZero.queue()
98+
99+
let result = awaiter.wait(timeout: DispatchTime.now().advanced(by: DispatchTimeInterval.seconds(10)))
100+
XCTAssertEqual(result, .success, "The Event Handler was not invoked in time!")
101+
XCTAssertEqual(self.myFoo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(self.myFoo)")
102+
103+
listenerHandler?.remove()
104+
}
79105
}

0 commit comments

Comments
 (0)