An easy way to implement events-driven architecture in your iOS project using the MERLin framework
If you are familiar with iOS development, you know how important it is to correctly decouple app components for testability and overall flexibility. Each feature should work independently from the others. Ideally, each feature should be easily replaceable with another component providing the same functionality. To achieve this result we embrace protocol oriented development and dependency injection. Most importantly, we formally structure our apps following well-known architectural design patterns.
Events-driven architecture is described by Wikipedia as a “software architecture pattern promoting the production, detection, consumption of, and reaction to events.”
In short, there are two main actors in this architectural design pattern: an event Producer and a Consumer of events (or Listener).
We define an event as a message sent by a system to notify listeners of a change of state. A producer of events is a component that is able to notify listeners of changes to its internal state. The only responsibility of the producer is to send these messages without the knowledge of the existence of a listener that will react to the emitted events.
A listener, on the other hand, is a system interested in events emitted by one or more producers. Its job is to listen to emitted events and react to them, producing an effect that will fulfill the purpose of the listener. An example would be a product list analytics events listener in an eCommerce app. It would listen to events generated by a products list producer and log to an analytics provider the events of “product selected”, “refinement button tapped” and so on…
As previously mentioned, it is possible to have many listeners for each producer instance. Each listener should have one unique responsibility (analytics, routing, internal logging and so on).
Such architectural design patterns bring many benefits to the project.
Loosely coupled components: Having a producer broadcast events ensures that the producing component does not have any knowledge of the receiver(s) of the message. Furthermore, neither the producer nor the receiver has knowledge of the other’s implementation details (usually masked by a protocol).
From this derives high modularity: As long as two producers produce the same set of events they can replace each other without affecting the listener’s implementation or other producers.
Parallelising development: After agreeing on the set of events that a specific producer can emit, two different devs can parallelize the development of listeners and producer. Producers can be mocked to trigger listeners reactions while the real implementation is under construction. From the producer point of view, it has no knowledge about the listeners that will subscribe to its events.
Testability: Producers can be tested independently from the rest of the app thanks to their own nature of isolated components. Producers can also be replaced by mocks emitting the same events expected by a specific listener, so listeners can be tested with mock producers.
In an events-driven architecture, it is important to have a solid events structure and a solid event dispatch mechanism. In iOS, unfortunately, none of this is available out-of-the-box. The only out-of-the-box events dispatch mechanism available to devs is the
NotificationCenter that lacks in type safety and events are basically strings.
That’s where MERLin comes into play. MERLin is a reactive framework that aims to simplify the adoption of an event-driven architecture within an iOS app. It emphasizes the concept of modularity, promoting an easy to implement communication channel to deliver events from producers to listeners.
MERLin uses RxSwift to establish a communication channel between producers and listeners.
Module: In MERLin, a module is a framework that exposes a specific functionality, an app feature.
- It’s independent or it has very limited dependencies.
- It should not know about other modules and does not expose implementation details.
- Any module can be a producer of events of a specific type and it must be contextualized (it needs a context to be built).
- Different modules providing the same functionality should ideally produce the same events.
In this example, the module provides a feature capable of showing a list of restaurants. It has to provide an
unmanagedRootViewController that will be the view controller that’s going to be shown on screen when needed.
The module is creating all the stack needed for
RestaurantsListViewController to work properly. In this specific implementation, there is a
RestaurantsListViewModel that will be able to emit events that happened on the view controller, using the
_events PublishSubject passed in the initializer. This is just one way of handling events emission. This can vary based on your implementation details.
Event: An event is named and unique within the same system domain and can have payloads describing the change that happened in the domain.
Different systems offering the same service should emit the same events.
In MERLin, modules can be event producers and emit events to signal a change of state or that an action was started/finished.
In MERLin, events are enums conforming the
In this example, we are defining a list of events of type
ProductDetailPageEvent. A module emitting these events uses this enum to publish all possible events it can send to listeners. MERLin provides an easy way to capture specific events from a stream of events:
Router: Some events might cause routing to another module of the app. The router is the object that makes the connection possible in terms of UI. You must build a
Router object in your app and it has to conform to the
Router protocol. In protocol extensions, MERLin offers many functions needed for the compliance to
Router. This makes the creation of a new router very simple; all of the complexity is reduced to defining the app’s root view controller:
In this example, the root view controller is a simple
UINavigationBar with a
restaurantList as root.
Listener: In MERLin an events consumer is called events listener.
An events listener reacts to a module’s events; It can listen to specific types of events, or to any event (from any producer). Some events listeners can cause routing within the app. In that case, we will call them Routing Events Listeners. These special listeners have a router to make routing to new modules possible.
Here three examples of events listeners:
Each listener should have only one responsibility. There is no limit to the number of listeners that can be listening to a specific module event channel, so it is possible to have a specific listener for each analytics provider, for each module.
There must be a certain point in time new modules are presented to existing events listeners. This happens in the
MERLin offers a ready to use implementation of the module manager. All you need to do is: inside your app delegate create a
moduleManager, create your listeners and pass the array of listeners to the instance of the
A router needs the module manager to transform a
routingStep into a view controller, so in practice, once this little setup is done, you’ll never have to deal with the
moduleManager directly. All you will have to do is to focus on writing modules and listeners.
RoutingEventsListeners will use the router with a routing step. The router will ask the
ModuleManager for a view controller for a specific step and the
moduleManager will create the right module, present the module to events listeners, extract the
viewController from it, and return the view controller to the router. During this phase, if a listener is interested in the events of the new producer, then it will subscribe.
Modules will remain alive for as long as the
ViewController is alive. When the
ViewController is deallocated the module instance will be deallocated as well.
There is so much more to tell about this framework. If you are curious, check it out on GitHub, read the documentation or wait for the next article where we will explore in detail
EventsListeners and will give hints on how to structure your project in a smart and scalable way taking advantage of