diff --git a/README.md b/README.md index e35f4e6..ae306a7 100644 --- a/README.md +++ b/README.md @@ -477,6 +477,37 @@ In the above code example, `maximumAge` is a value defined in *nanoseconds*. Wit This functionality is very useful when the context of an *Event*'s usage would have a known, fixed expiry. +## `EventListener` with *Custom Event Filtering* Interest +Version 5.2.0 of this library introduces the concept of *Custom Event Filtering* for *Listeners*. + +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*. + +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: +```swift +class TemperatureRatingViewModel: ObservableObject { + @Published var temperatureInCelsius: Float + @Published var temperatureRating: TemperatureRating + + var listenerHandle: EventListenerHandling? + + internal func onTemperatureRatingEvent(_ event: TemperatureRatingEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) { + temperatureInCelsius = event.temperatureInCelsius + temperatureRating = event.temperatureRating + } + + internal func onTemperatureRatingEventFilter(_ event: TemperatureRatingEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> Bool { + if event.temperatureInCelsius > 50 { return false } // If the Temperature is above 50 Degrees, this Listener is not interested in it! + return true // If the Temperature is NOT above 50 Degrees, the Listener IS interested in it! + } + + init() { + // Let's register our Event Listener Callback! + listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, interestedIn: .custom, customFilter: onTemperatureRatingEventFilter) + } +} +``` +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*. + ## `EventPool` 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. diff --git a/Sources/EventDrivenSwift/Central/EventCentral.swift b/Sources/EventDrivenSwift/Central/EventCentral.swift index 36cddd5..6d3aae1 100644 --- a/Sources/EventDrivenSwift/Central/EventCentral.swift +++ b/Sources/EventDrivenSwift/Central/EventCentral.swift @@ -75,8 +75,8 @@ final public class EventCentral: EventDispatcher, EventCentralable { } } - @discardableResult @inline(__always) public static func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0) -> EventListenerHandling where TEvent : Eventable { - return _shared.eventListener.addListener(requester, callback, forEventType: forEventType, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge) + @discardableResult @inline(__always) public static func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0, customFilter: TypedEventFilterCallback? = nil) -> EventListenerHandling where TEvent : Eventable { + return _shared.eventListener.addListener(requester, callback, forEventType: forEventType, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge, customFilter: customFilter) } @inline(__always) public static func removeListener(_ token: UUID) { diff --git a/Sources/EventDrivenSwift/Central/EventCentralable.swift b/Sources/EventDrivenSwift/Central/EventCentralable.swift index 6bbe0ca..2862094 100644 --- a/Sources/EventDrivenSwift/Central/EventCentralable.swift +++ b/Sources/EventDrivenSwift/Central/EventCentralable.swift @@ -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(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling + @discardableResult static func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64, customFilter: TypedEventFilterCallback?) -> EventListenerHandling /** Locates and removes the given Listener `token` (if it exists) from the Central Event Listener diff --git a/Sources/EventDrivenSwift/Event/Eventable.swift b/Sources/EventDrivenSwift/Event/Eventable.swift index c89d17d..034537f 100644 --- a/Sources/EventDrivenSwift/Event/Eventable.swift +++ b/Sources/EventDrivenSwift/Event/Eventable.swift @@ -63,7 +63,7 @@ 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(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling + @discardableResult static func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64, customFilter: TypedEventFilterCallback?) -> EventListenerHandling /** Locates and removes the given Listener `token` (if it exists) from the Central Event Listener @@ -122,8 +122,8 @@ extension Eventable { EventCentral.scheduleStack(self, at: at, priority: priority) } - @discardableResult static public func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0) -> EventListenerHandling { - return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge) + @discardableResult static public func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0, customFilter: TypedEventFilterCallback? = nil) -> EventListenerHandling { + return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge, customFilter: customFilter) } public static func removeListener(_ token: UUID) { diff --git a/Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift b/Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift index e466e45..577a7f4 100644 --- a/Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift +++ b/Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift @@ -54,7 +54,7 @@ open class EventDispatcher: EventHandler, EventDispatching { _receivers.withLock { receivers in var bucket = receivers[eventTypeName] - if bucket == nil { return } /// Can't remove a Receiver if there isn't even a Bucket for hte Event Type + if bucket == nil { return } /// Can't remove a Receiver if there isn't even a Bucket for the Event Type /// Remove any Receivers from this Event-Type Bucket for the given `receiver` instance. bucket!.removeAll { receiverContainer in diff --git a/Sources/EventDrivenSwift/EventListener/EventListenable.swift b/Sources/EventDrivenSwift/EventListener/EventListenable.swift index dbcfea6..3f491fa 100644 --- a/Sources/EventDrivenSwift/EventListener/EventListenable.swift +++ b/Sources/EventDrivenSwift/EventListener/EventListenable.swift @@ -14,6 +14,7 @@ import Foundation - Version: 3.0.0 */ public typealias EventCallback = (_ event: any Eventable, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> () +public typealias EventFilterCallback = (_ event: any Eventable, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> Bool /** Convienience `typealias` used for Typed Event Callbacks @@ -63,7 +64,7 @@ public protocol EventListenable: AnyObject, EventReceiving { - 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 - Returns: A `UUID` value representing the `token` associated with this Event Callback */ - @discardableResult func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling + @discardableResult func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64, customFilter: TypedEventFilterCallback?) -> EventListenerHandling /** Locates and removes the given Listener `token` (if it exists) diff --git a/Sources/EventDrivenSwift/EventListener/EventListener.swift b/Sources/EventDrivenSwift/EventListener/EventListener.swift index df179a7..9a92c87 100644 --- a/Sources/EventDrivenSwift/EventListener/EventListener.swift +++ b/Sources/EventDrivenSwift/EventListener/EventListener.swift @@ -34,6 +34,7 @@ open class EventListener: EventHandler, EventListenable { var executeOn: ExecuteEventOn = .requesterThread var interestedIn: EventListenerInterest = .all var maximumEventAge: UInt64 = 0 + var customFilter: EventFilterCallback? } /** @@ -73,6 +74,8 @@ open class EventListener: EventHandler, EventListenable { 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! + if listener.interestedIn == .custom && (listener.customFilter == nil || !listener.customFilter!(event.event, priority, event.dispatchTime)) { continue } + switch listener.executeOn { case .requesterThread: 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 { } } - @discardableResult public func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0) -> EventListenerHandling { + @discardableResult public func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0, customFilter: TypedEventFilterCallback? = nil) -> EventListenerHandling { let eventTypeName = forEventType.getEventTypeName() let method: EventCallback = { event, priority, dispatchTime in self.callTypedEventCallback(callback, forEvent: event, priority: priority, dispatchTime: dispatchTime) } - let eventListenerContainer = EventListenerContainer(requester: requester, callback: method, dispatchQueue: OperationQueue.current?.underlyingQueue, executeOn: executeOn, interestedIn: interestedIn, maximumEventAge: maximumAge) + var filterMethod: EventFilterCallback? = nil + if customFilter != nil { + filterMethod = { event, priority, dispatchTime in + self.callTypedEventFilterCallback(customFilter!, forEvent: event, priority: priority, dispatchTime: dispatchTime) + } + } + let eventListenerContainer = EventListenerContainer(requester: requester, callback: method, dispatchQueue: OperationQueue.current?.underlyingQueue, executeOn: executeOn, interestedIn: interestedIn, maximumEventAge: maximumAge, customFilter: filterMethod) _eventListeners.withLock { eventCallbacks in var bucket = eventCallbacks[eventTypeName] if bucket == nil { bucket = [EventListenerContainer]() } // Create a new bucket if there isn't already one! @@ -161,10 +170,28 @@ open class EventListener: EventHandler, EventListenable { - callback: The code (Closure or Callback Method) to execute for the given `forEvent`, typed generically using `TEvent` - forEvent: The instance of the `Eventable` type to be processed - priority: The `EventPriority` with which the `forEvent` was dispatched + - dispatchTime: The `DispatchTime` at which `forEvent` was Dispatched */ internal func callTypedEventCallback(_ callback: @escaping TypedEventCallback, forEvent: Eventable, priority: EventPriority, dispatchTime: DispatchTime) { if let typedEvent = forEvent as? TEvent { callback(typedEvent, priority, dispatchTime) } } + + /** + Performs a Transparent Type Test, Type Cast, and Method Call to the Custom Filter via a `callback` Closure. + - Author: Simon J. Stuart + - Version: 5.2.0 + - Parameters: + - 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 + - forEvent: The instance of the `Eventable` type to be processed + - priority: The `EventPriority` with which the `forEvent` was dispatched + - dispatchTime: The `DispatchTime` at which `forEvent` was Dispatched + */ + internal func callTypedEventFilterCallback(_ callback: @escaping TypedEventFilterCallback, forEvent: Eventable, priority: EventPriority, dispatchTime: DispatchTime) -> Bool { + if let typedEvent = forEvent as? TEvent { + return callback(typedEvent, priority, dispatchTime) + } + return false /// We will simply return `false` if the Event is of the wrong Type + } } diff --git a/Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift b/Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift index cf9b034..4efad8f 100644 --- a/Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift +++ b/Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift @@ -25,4 +25,13 @@ public enum EventListenerInterest: CaseIterable { - Version: 5.0.0 */ case youngerThan + + /** + Receivers will ignore any Event where the Filter Callback returns `false`, and accept any Event where the Filter Callback returns `true` + - Author: Simon J. Stuart + - Version: 5.2.0 + */ + case custom } + +public typealias TypedEventFilterCallback = (_ event: EventType, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> Bool diff --git a/Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift b/Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift index 8acde01..65447fb 100644 --- a/Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift +++ b/Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift @@ -37,6 +37,7 @@ final class BasicEventListenerTests: XCTestCase, EventListening { var myFoo = 0 var listenerHandler: EventListenerHandling? = nil let testOne = TestEventTypeOne(foo: 1000) // Create the Event + let testZero = TestEventTypeOne(foo: 2000) // Create the Event var awaiter = DispatchSemaphore(value: 0) func testEventListenerOnListenerThread() throws { @@ -76,4 +77,29 @@ final class BasicEventListenerTests: XCTestCase, EventListening { listenerHandler?.remove() } + + func testEventListenerCustomFilter() throws { + XCTAssertEqual(myFoo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(myFoo)") + + listenerHandler = TestEventTypeOne.addListener(self, { (event: TestEventTypeOne, priority, dispatchTime) in + self.myFoo = event.foo + self.awaiter.signal() + }, executeOn: .taskThread, interestedIn: .custom, customFilter: { (event: TestEventTypeOne, priority, dispatchTime) in + if event.foo == 1000 { + print("Accepting Event because foo = 1000") + return true + } + print("Ignoring Event where foo = \(event.foo)") + return false + }) + + testOne.queue() + testZero.queue() + + let result = awaiter.wait(timeout: DispatchTime.now().advanced(by: DispatchTimeInterval.seconds(10))) + XCTAssertEqual(result, .success, "The Event Handler was not invoked in time!") + XCTAssertEqual(self.myFoo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(self.myFoo)") + + listenerHandler?.remove() + } }