Skip to content

Usage Code Examples

Simon J Stuart edited this page Sep 5, 2022 · 1 revision

So, now that we've taken a look at what EventDrivenSwift is, what it does, and we've covered a lot of the important Terminology, let's take a look at how we can actually use it.

Defining an Event type

You can make virtually any struct type into an Event type simply by inheriting from Eventable:

struct TemperatureEvent: Eventable {
    var temperatureInCelsius: Float
} 

It really is as simple as that!

Dispatching an Event

Now that we have a defined Event type, let's look at how we would Dispatch an Event of this type:

let temperatureEvent = TemperatureEvent(temperatureInCelsius: 23.25)

The above creates an instance of our TemperatureEvent Event type. If we want to dispatch it via a Queue with the .normal Priority, we can do so as easily as this:

temperatureEvent.queue()

We can also customise the Priority:

temperatureEvent.queue(priority: .highest)

The above would dispatch the Event via a Stack with the .highest Priority.

The same works when dispatching via a Stack:

temperatureEvent.stack()

Above would again be with .normal Priority...

temperatureEvent.stack(priority: .highest)

Above would be with .highest Priority.

Scheduled Dispatching of an Event

Version 4.2.0 introduced Scheduled Dispatch into the library:

temperatureEvent.scheduleQueue(at: DispatchTime.now() + TimeInterval().advanced(by: 4), priority: .highest)

The above would Dispatch the temperatureEvent after 4 seconds, via the Queue, with the highest Priority

temperatureEvent.scheduleStack(at: DispatchTime.now() + TimeInterval().advanced(by: 4), priority: .highest)

The above would Dispatch the temperatureEvent after 4 seconds, via the Stack, with the highest Priority

Scheduled Event Dispatch is a massive advantage when your use-case requires a fixed or calculated time delay between the composition of an Event, and its Dispatch for processing.

(Receiving & Processing Events - Method 1) Defining an EventThread

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 a way to Receive our *TemperatureEvents so that we can do something with them. One way of doing this is to define an EventThreadto listen for and process ourTemperatureEvent`s.

class TemperatureProcessor: EventThread {
    /// Register our Event Listeners for this EventThread
    override func registerEventListeners() {
        addEventCallback(onTemperatureEvent, forEventType: TemperatureEvent.self)
    }
    
    /// Define our Callback Function to process received TemperatureEvent Events
    func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {

    }
}

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.

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

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.

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.

Version 5.0.0 introduces the new parameter, dispatchTime, which will always provide the DispatchTime reference at which the Event was Dispatched. You can use this to determine Delta (how much time has passed since the Event was Dispatched), which is particularly useful if you are performing interpolation and/or extrapolation.

Now, let's actually do something with our TemperatureEvent in the onTemperatureEvent method.

    /// An Enum to map a Temperature value onto a Rating
    enum TemperatureRating: String {
        case belowFreezing = "Below Freezing"
        case freezing = "Freezing"
        case reallyCold = "Really Cold"
        case cold = "Cold"
        case chilly = "Chilly"
        case warm = "Warm"
        case hot = "Hot"
        case reallyHot = "Really Hot"
        case boiling = "Boiling"
        case aboveBoiling = "Steam"
        
        static func fromTemperature(temperatureInCelsius: Float) -> TemperatureRating {
            if temperatureInCelsius < 0 { return .belowFreezing }
            else if temperatureInCelsius == 0 { return .freezing }
            else if temperatureInCelsius < 5 { return .reallyCold }
            else if temperatureInCelsius < 10 { return .cold }
            else if temperatureInCelsius < 16 { return .chilly }
            else if temperatureInCelsius < 22 { return .warm }
            else if temperatureInCelsius < 25 { return .hot }
            else if temperatureInCelsius < 100 { return .reallyHot }
            else if temperatureInCelsius == 100 { return .boiling }
            else { return .aboveBoiling }
        }
    }
    
    @ThreadSafeSemaphore public var temperatureInCelsius: Float = Float.zero
    @ThreadSafeSemaphore public var temperatureRating: TemperatureRating = .freezing
    
    func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {
        temperatureInCelsius = event.temperatureInCelsius
        temperatureRating = TemperatureRating.fromTemperature(event.temperatureInCelsius)
    }
}

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 EventThread, and also pre-calculates a TemperatureRating based on the Temperature value received in the Event.

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

Playground Code to test everything so far

The only thing you're missing so far is how to create an instance of your EventListner type. This is in fact remarkably simple. The following can be run in a Playground:

let temperatureProcessor = TemperatureProcessor()

That's all you need to do to create an instance of your TemperatureProcessor.

Let's add a line to print the inital values of temperatureProcessor:

print("Temp in C: \(temperatureProcessor.temperatureInCelsius)")
print("Temp Rating: \(temperatureProcessor.temperatureRating)")

We can now dispatch a TemperatureEvent to be processed by temperatureProcessor:

TemperatureEvent(temperatureInCelsius: 25.5).queue()

Because Events are processed Asynchronously, and because this is just a Playground test, let's add a 1-second sleep to give TemperatureProcessor time to receive and process the Event. Note: In reality, this would need less than 1ms to process!

sleep(1)

Now let's print the same values again to see that they have changed:

print("Temp in C: \(temperatureProcessor.temperatureInCelsius)")
print("Temp Rating: \(temperatureProcessor.temperatureRating)")

Now you have a little Playground code to visually confirm that your Events are being processed. You can modify this to see what happens.

Observing an EventThread

Remember, EventThreads are also Observable, so we can not only receive and operate on Events, we can also notify Observers in response to Events.

Let's take a look at a simple example based on the examples above. We shall begin by defining an Observer Protocol:

protocol TemperatureProcessorObserver: AnyObject {
    func onTemperatureEvent(temperatureInCelsius: Float)
}

Now let's modify the onTemperatureEvent method we implemented in the previous example:

    func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {
        temperatureInCelsius = event.temperatureInCelsius
        temperatureRating = TemperatureRating.fromTemperature(event.temperatureInCelsius)
        
        /// Notify any Observers...
        withObservers { (observer: TemperatureProcessorObserver) in
            observer.onTemperatureEvent(temperatureInCelsius: event.temperatureInCelsius)
        }
    }

Now, every time a TemperatureEvent is processed by the EventThread, it will also notify any direct Observers as well.

It should be noted that this functionality serves as a complement to Event-Driven behaviour, as there is no "one size fits all" solution to every requirement in software. It is often neccessary to combine methodologies to achieve the best results.

Reciprocal Events

Typically, systems not only consume information, but also return information (results). This is not only true when it comes to Event-Driven systems, but also trivial to achieve.

Let's expand upon the previous example once more, this time emitting a reciprocal Event to encapsulate the Temperature, as well as the TemperatureRating we calculated in response to the TemperatureEvent.

We'll begin by defining the Reciprocal Event type:

enum TemperatureRatingEvent: Eventable {
    var temperatureInCelsius: Float
    var temperatureRating: TemperatureRating
}

With the Event type defined, we can now once more expand our onTemperatureEvent to Dispatch our reciprocal TemperatureRatingEvent:

    func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {
        temperatureInCelsius = event.temperatureInCelsius
        temperatureRating = TemperatureRating.fromTemperature(event.temperatureInCelsius)
        
        /// Notify any Observers...
        withObservers { (observer: TemperatureProcessorObserver) in
            observer.onTemperatureEvent(temperatureInCelsius: event.temperatureInCelsius)
        }
        
        /// Dispatch our Reciprocal Event...
        TemperatureRatingEvent(
            temperatureInCelsius = temperatureInCelsius,
            temperatureRating = temperatureRating
        ).queue()
    }

As you can see, we can create and Dispatch an Event in a single operation. This is because Events should be considered to be "fire and forget". You need only retain a copy of the Event within the Dispatching Method if you wish to use its values later in the same operation. Otherwise, just create it and Dispatch it together, as shown above.

Now that we've walked through these basic Usage Examples, see if you can produce your own EventThread to process TemperatureRatingEvents. Everything you need to achieve this has already been demonstrated in this document.

UIEventThread

Version 2.0.0 introduced the UIEventThread base class, which operates exactly the same way as EventThread, 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 UIEventThread instead of EventThread whenever it is imperative for one or more Event Callbacks to execute on the MainActor.

(Receiving & Processing Events - Method 2) EventListener

Version 3.0.0 introduced the EventListener concept to the Library. These are a universally-available means (available in any class you define) of Receiving Events dispatched from anywhere in your code, and require considerably less code to use.

An EventListener is a universal way of subscribing to Events, anywhere in your code, without having to define and operate within the constraints of an EventThread.

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.

Important Note: EventListener will (by default) 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.

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.

For the sake of this example, let's define this the pure SwiftUI way, without taking advantage of our Observable library:

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
    }
    
    init() {
        // Let's register our Event Listener Callback!
        listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent)
    }
}

It really is that simple!

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.

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!

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!

If you need your Event Callback to execute on the Listener's Thread, as of Version 3.1.0... you can!

listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, executeOn: .listenerThread)

Remember: When executing an Event Callback on .listenerThread, you will need to ensure that your Callback and all resources that it uses are 100% Thread-Safe! Important: Executing the Event Callback on .listnerThread can potentially delay the invocation of other Event Callbacks. Only use this option when it is necessary.

You can also execute your Event Callback on an ad-hoc Task:

listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, executeOn: .taskThread)

Remember: When executing an Event Callback on .taskThread, you will need to ensure that your Callback and all resources that it uses are 100% Thread-Safe!

Another thing to note about the above example is the listenerHandle. Whenever you register a Listener, it will return an EventListenerHandling object. You can use this value to Unregister your Listener at any time:

    listenerHandle.remove()

This will remove your Listener Callback, meaning it will no longer be invoked any time a TemperatureRatingEvent is Dispatched.

Note: This is an improvement for Version 4.1.0, as opposed to the use of an untyped UUID from previous versions.

EventListeners are an extremely versatile and very powerful addition to EventDrivenSwift.

EventListener with Latest-Only Interest

Version 4.3.0 of this library introduces the concept of Latest-Only Listeners. A Latest-Only Listener is a Listener that will only be invoked for the very latest Event of its requested Event Type. If there are a number of older Events of this type pending in a Queue/Stack, they will simply be skipped over... and only the very Latest will invoke your Listener.

We have made it incredibly simple for you to configure your Listener to be a Latest-Only Listener. Taking the previous code example, we can simply modify it as follows:

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
    }
    
    init() {
        // Let's register our Event Listener Callback!
        listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, interestedIn: .latestOnly)
    }
}

By including the interestedIn optional parameter when invoking addListener against any Eventable type, and passing for this parameter a value of .latestOnly, we define that this Listener is only interested in the Latest TemperatureRatingEvent to be Dispatched. Should a number of TemperatureRatingEvents build up in the Queue/Stack, the above-defined Listener will simply discard any older Events, and only invoke for the newest.

EventListener with Maximum Age Interest

Version 5.1.0 of this library introduces the concent of Maximum Age Listeners. A Maximum Age Listener is a Listener that will only be invoked for Events of its registered Event Type that are younger than a defined Maximum Age. Any Event older than the defined Maximum Age will be skipped over, while any Event younger will invoke your Listener.

We have made it simple for you to configure your Listener to define a Maximum Age interest. Taking the previous code example, we can simply modify it as follows:

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
    }
    
    init() {
        // Let's register our Event Listener Callback!
        listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, interestedIn: .youngerThan, maximumAge: 1 * 1_000_000_000)
    }
}

In the above code example, maximumAge is a value defined in nanoseconds. With that in mind, 1 * 1_000_000_000 would be 1 second. This means that, any TemperatureRatingEvent older than 1 second would be ignored by the Listener, while any TemperatureRatingEvent younger than 1 second would invoke the onTemperatureRatingEvent method.

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:

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 EventThreads, where inbound Events will be directed to the best EventThread in the EventPool at any given moment.

EventDrivenSwift makes it trivial to produce an EventPool for any given EventThread type.

To create an EventPool of our TemperatureProcessor example from earlier, we can use a single line of code:

var temperatureProcessorPool = EventPool<TemperatureProcessor>(capacity: 5)

The above example will create an EventPool of TemperatureProcessors, with an initial Capacity of 5 instances. This means that your program can concurrently process 5 TemperatureEvents. Obviously, for a process so simple and quick to complete as our earlier example, it would not be neccessary to produce an EventPool, but you can adapt this example for your own, more complex and time-consuming, EventThread implementations to immediately parallelise them.

EventPools enable you to specify the most context-appropriate Balancer on initialization:

var temperatureProcessorPool = EventPool<TemperatureProcessor>(capacity: 5, balancer: EventPoolRoundRobinBalancer())

The above example would use the EventPoolRoundRobinBalancer implementation, which simply directs each inbound Eventable to the next EventThread in the pool, rolling back around to the first after using the final EventThread in the pool.

There is also another Balancer available in version 4.0.0:

var temperatureProcessorPool = EventPool<TemperatureProcessor>(capacity: 5, balancer: EventPoolLowestLoadBalancer())

The above example would use the EventPoolLowestLoadBalancer implementation, which simply directs each inbound Eventable to the EventThread in the pool with the lowest number of pending Eventables in its own Queue and Stack.

NOTE: When no balancer is declared, EventPool will use EventPoolRoundRobinBalancer by default.