-
Notifications
You must be signed in to change notification settings - Fork 13
Fuser
Rendering UIs is only half of the puzzle. We also need to be able to define how our application should respond to user interaction. We identified the following pattern:
- Register a listener, i.e.
view.addTarget(self, #selector(..), for: .someInteractionEvent)
- Transform this event into data which can be understood by our application
- Unregister the listener when finished, i.e.
view.removeTarget(self, #selector(..), for: .someInteractionEvent)
. While this step is not always necessary, we consider it good practice.
There are a number of problems with this approach.
-
addTarget
takes a selector. This is inherently more complicated than a lambda function. - It is easy to forget to call
removeTarget
, or to call it with the wrong parameters. - These APIs do not compose
The Fuser provides a safer and more composable abstraction for this pattern.
We start by creating a type which represents everything that can happen in our UI. This type is usually modelled as an enum with associated data. The simplest instance of a Fuser is a listener for one kind of event (e.g. tapping up) on one view. Tapping the view will produce an event which contains information about the type of event which took place. In this case, it would be a UITapGestureRecognizer
. We can call extract
on this event to transform it into the UI interaction type that our application understands.
For example:
.extract(UIEvents.viewWasTapped, .fromTaps(view))
Notice the symmetry between the Diffuser
and the Fuser
. into
in the Diffuser corresponds to from
in the Fuser and map
in the Diffuser corresponds to extract
in the Fuser.
We can combine multiple listeners with fromAll
, assuming they produce events of the same type. If they don't, simply extract
them into the same type:
let otherEventFuser: Fuser<OtherEvent> = ...
let fuser: Fuser<UIEvents> = fromAll(
.extract( .viewWasTapped, .fromEvents(view.button, for: .touchUpInside)),
.extract( .otherViewWasTapped, .fromEvents(view.otherButton, for: .touchUpInside)),
.extract(otherEventToUIEvent, otherEventFuser)
)
On its own, this fuser will not do anything. We can "activate" it by providing it with a function for it to call when an event is generated:
let disposable = fuser.connect { event in
// do something with the event here
}
This call generates a Disposable
, which contains a single method which we call when we want to tear down all listeners:
connection.dispose()
A Fuser needs to take a function which, when called, will forward events to other parts of our system. We call this dispatching events. When given such a function, i.e. when connect
is called, it should start dispatching its events to that function and return a Disposable
.
In code, this is represented by the from
function (@escaping
s are omitted for brevity):
func from<A> (_ instance: ((A) -> ()) -> Disposable) -> Fuser<A>
On its own, this signature is pretty dense, so here is an example of what creating a click listener for a button might look like:
func fromTaps(_ view: UIButton) -> Fuser<UIButton> {
let connectable: (@escaping (UIButton) -> ()) -> Disposable = { dispatch in
let action = Action<UIButton>(dispatch) // `Action` is just a a wrapper for objc selectors
view.addTarget(action, action: action.selector, for: .touchUpInside)
return AnonymousDisposable {
view.removeTarget(action, action: action.selector, for: events)
}
}
return from(connectable)
}
Let's break down what is going on here:
- We create a lambda which take the
dispatch
parameter. Remember,dispatch
is what will forward our events. - We create an Objective-C selector which simply calls
dispatch
using theAction
utility class. - We call
addTarget
to setup a listener which will call this selector. - We return a
Disposable
which, when called, callsremoveTarget
with the desired arguments.
On its own, this function isn't very reuseable. We could improve this by using generics and by parameterizing on the event type to encapsulate this behaviour for all UIControlEvents
on all UIControl
sub-classes. Luckily, the Fuser library
already provides this functionality. It also has a function for adding arbitrary UIGestureRecognizer
s to arbitrary views.
Fusers allow us to merge all of our UI's event sources into one stream of events which we can handle uniformly. If the types of these event sources don't match up, we use extract
to convert between types.
The Diffuser gave us a single entry point for data to flow into our UI, and the Fuser provides a single exit point for data to flow back out into our system. This symmetry allows us to construct views in a concise, declarative, and reactive manner.