diff --git a/README.md b/README.md index db31e0e..882ba74 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/Flowduino/EventDrivenSwift.git", - .upToNextMajor(from: "1.1.0") + .upToNextMajor(from: "2.0.0") ), ], //... @@ -189,13 +189,13 @@ Above would be with `.highest` *Priority*. ### Defining an `EventReceiver` So, we have an *Event* type, and we are able to *Dispatch* it through a *Queue* or a *Stack*, with whatever *Priority* we desire. Now we need to define an `EventReceiver` to listen for and process our `TemperatureEvent`s. +**Note:** Code example in this section was updated for Version 2.0.0 due to considerable improvements, which necessitated changing the Interface slightly (for the better) + ```swift class TemperatureProcessor: EventReceiver { /// Register our Event Listeners for this EventReceiver override func registerEventListeners() { - addEventCallback({ event, priority in - self.callTypedEventCallback(self.onTemperatureEvent, forEvent: event, priority: priority) - }, forEventType: TemperatureEvent.self) + addEventCallback(onTemperatureEvent, forEventType: TemperatureEvent.self) } /// Define our Callback Function to process received TemperatureEvent Events @@ -210,13 +210,8 @@ Firstly, `TemperatureProcessor` inherits from `EventReceiver`, which is where al The function `registerEventListeners` will be called automatically when an instance of `TemperatureProcessor` is created. Within this method, we call `addEventCallback` to register `onTemperatureEvent` so that it will be invoked every time an *Event* of type `TemperatureEvent` is *Dispatched*. -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` - 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`. -**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. -For the moment, this solution works well, and enables you to begin using `EventDrivenSwift` in your applications immediately. - Now, let's actually do something with our `TemperatureEvent` in the `onTemperatureEvent` method. ```swift /// An Enum to map a Temperature value onto a Rating @@ -350,12 +345,14 @@ As you can see, we can create and *Dispatch* an *Event* in a single operation. T 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. +## `UIEventReceiver` +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`. + ## Features Coming Soon `EventDrivenSwift` is an evolving and ever-improving Library, so here is a list of the features you can expect in future releases: - **Event Pools** - A superset expanding upon a given `EventReceiver` descendant type to provide pooled processing based on given scaling rules and conditions. -- **`UIEventReceiver`** - Will enable you to register Event Listener Callbacks to be executed on the UI Thread. This is required if you wish to use Event-Driven behaviour to directly update SwiftUI Views, for example. -These are the features intended for the next Release, which will either be *1.2.0* or *2.0.0* depending on whether these additions require interface-breaking changes to the interfaces in version *1.1.0*. +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*. ## License diff --git a/Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift b/Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift index bc547bd..17f3aba 100644 --- a/Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift +++ b/Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift @@ -60,16 +60,18 @@ open class EventReceiver: EventHandler, EventReceivable { /** Registers an Event Callback for the given `Eventable` Type - Author: Simon J. Stuart - - Version: 1.0.0 + - Version: 2.1.0 - Parameters: - callback: The code to invoke for the given `Eventable` Type - forEventType: The `Eventable` Type for which to Register the Callback */ - internal func addEventCallback(_ callback: @escaping EventCallback, forEventType: Eventable.Type) { + internal func addEventCallback(_ callback: @escaping TypedEventCallback, forEventType: Eventable.Type) { let eventTypeName = String(reflecting: forEventType) _eventCallbacks.withLock { eventCallbacks in - eventCallbacks[eventTypeName] = callback + eventCallbacks[eventTypeName] = { event, priority in + self.callTypedEventCallback(callback, forEvent: event, priority: priority) + } } /// We automatically register the Listener with the Central Event Dispatcher @@ -79,7 +81,7 @@ open class EventReceiver: EventHandler, EventReceivable { /** Performs a Transparent Type Test, Type Cast, and Method Call via the `callback` Closure. - Author: Simon J. Stuart - - Version: 1.0.0 + - Version: 2.1.0 - Parameters: - 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 diff --git a/Sources/EventDrivenSwift/UIEventReceiver/UIEventReceivable.swift b/Sources/EventDrivenSwift/UIEventReceiver/UIEventReceivable.swift new file mode 100644 index 0000000..6902881 --- /dev/null +++ b/Sources/EventDrivenSwift/UIEventReceiver/UIEventReceivable.swift @@ -0,0 +1,19 @@ +// +// UIEventReceivable.swift +// Copyright (c) 2022, Flowduino +// Authored by Simon J. Stuart on 11th August 2022 +// +// Subject to terms, restrictions, and liability waiver of the MIT License +// + +import Foundation + +/** + Protocol describing anything that Receives Events on the UI Thread + - Author: Simon J. Stuart + - Version: 2.1.0 + - Note: Inherits from `EventReceivable` + */ +public protocol UIEventReceivable: AnyObject, EventReceivable { + +} diff --git a/Sources/EventDrivenSwift/UIEventReceiver/UIEventReceiver.swift b/Sources/EventDrivenSwift/UIEventReceiver/UIEventReceiver.swift new file mode 100644 index 0000000..30e4a0b --- /dev/null +++ b/Sources/EventDrivenSwift/UIEventReceiver/UIEventReceiver.swift @@ -0,0 +1,26 @@ +// +// UIEventReceiver.swift +// Copyright (c) 2022, Flowduino +// Authored by Simon J. Stuart on 11th August 2022 +// +// Subject to terms, restrictions, and liability waiver of the MIT License +// + +import Foundation + +/** + Abstract Base Type for all `UIEventRecevier` Thread Types. + - Author: Simon J. Stuart + - Version: 2.1.0 + - 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) + - Note: Your Event Handlers/Listeners/Callbacks will be executed on the UI Thread every time. + */ +open class UIEventReceiver: EventReceiver, UIEventReceivable { + override internal func callTypedEventCallback(_ callback: @escaping TypedEventCallback, forEvent: Eventable, priority: EventPriority) { + Task { /// Have to use a Task because this method is not `async` + await MainActor.run { /// Forces the call to be invoked on the `MainActor` (UI Thread) + super.callTypedEventCallback(callback, forEvent: forEvent, priority: priority) + } + } + } +} diff --git a/Tests/EventDrivenSwiftTests/BasicEventReceiverTests.swift b/Tests/EventDrivenSwiftTests/BasicEventReceiverTests.swift index 72d1c4e..88dda2a 100644 --- a/Tests/EventDrivenSwiftTests/BasicEventReceiverTests.swift +++ b/Tests/EventDrivenSwiftTests/BasicEventReceiverTests.swift @@ -25,9 +25,7 @@ final class BasicEventReceiverTests: XCTestCase { } override func registerEventListeners() { - addEventCallback({ event, priority in - self.callTypedEventCallback(self.eventOneCallback, forEvent: event, priority: priority) - }, forEventType: TestEventTypeOne.self) + addEventCallback(self.eventOneCallback, forEventType: TestEventTypeOne.self) } } diff --git a/Tests/EventDrivenSwiftTests/UIEventReceiverTests.swift.swift b/Tests/EventDrivenSwiftTests/UIEventReceiverTests.swift.swift new file mode 100644 index 0000000..828c9ed --- /dev/null +++ b/Tests/EventDrivenSwiftTests/UIEventReceiverTests.swift.swift @@ -0,0 +1,114 @@ +// +// UIEventReceiverTests.swift.swift +// +// +// Created by Simon Stuart on 11/08/2022. +// + +import XCTest +import ThreadSafeSwift +@testable import EventDrivenSwift + +final class UIEventReceiverTests_swift: XCTestCase { + struct TestEventTypeOne: Eventable { + var foo: Int + } + + class TestEventThread: UIEventReceiver { + @ThreadSafeSemaphore var foo: Int = 0 + + internal func eventOneCallback(_ event: TestEventTypeOne, _ priority: EventPriority) { + foo = event.foo + } + + override func registerEventListeners() { + addEventCallback(self.eventOneCallback, forEventType: TestEventTypeOne.self) + } + } + + let expectedTestOneFoo: Int = 1000 + + /** + I need to find a way of Unit Testing the `UIEventReceiver`. + It works, this I know, but Unit Tests operate on the UI Thread, which means they are blocking the `UIEventReceiver` callback until *after* the Test Method has already returned (thus failed) + */ + + /* + func testEventDispatchQueueDirect() throws { + let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event + let eventThread = TestEventThread() // Create the Thread + + XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)") + + eventThread.queueEvent(testOne, priority: .normal) // Now let's dispatch our Event to change this value + + sleep(5) + + XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)") + } + + func testEventDispatchQueueCentral() throws { + let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event + let eventThread = TestEventThread() // Create the Thread + + XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)") + EventCentral.queueEvent(testOne, priority: .normal) // Now let's dispatch our Event to change this value + + sleep(5) + + XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)") + } + + func testEventDispatchQueueTransparent() throws { + let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event + let eventThread = TestEventThread() // Create the Thread + + XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)") + + testOne.queue() // Now let's dispatch our Event to change this value + + sleep(5) + + XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)") + } + + func testEventDispatchStackDirect() throws { + let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event + let eventThread = TestEventThread() // Create the Thread + + XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)") + + eventThread.stackEvent(testOne, priority: .normal) // Now let's dispatch our Event to change this value + + sleep(5) + + XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)") + } + + func testEventDispatchStackCentral() throws { + let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event + let eventThread = TestEventThread() // Create the Thread + + XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)") + + EventCentral.stackEvent(testOne, priority: .normal) // Now let's dispatch our Event to change this value + + sleep(5) + + XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)") + } + + func testEventDispatchStackTransparent() throws { + let testOne = TestEventTypeOne(foo: expectedTestOneFoo) // Create the Event + let eventThread = TestEventThread() // Create the Thread + + XCTAssertEqual(eventThread.foo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(eventThread.foo)") + + testOne.stack() // Now let's dispatch our Event to change this value + + sleep(5) + + XCTAssertEqual(eventThread.foo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(eventThread.foo)") + } + */ +}