Skip to content
Jesper Sandström edited this page Nov 2, 2020 · 2 revisions

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:

  1. Register a listener, i.e. view.addTarget(self, #selector(..), for: .someInteractionEvent)
  2. Transform this event into data which can be understood by our application
  3. 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.

Fuser

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()

Essence of the Fuser

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 (@escapings 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:

  1. We create a lambda which take the dispatch parameter. Remember, dispatch is what will forward our events.
  2. We create an Objective-C selector which simply calls dispatch using the Action utility class.
  3. We call addTarget to setup a listener which will call this selector.
  4. We return a Disposable which, when called, calls removeTarget 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 UIGestureRecognizers to arbitrary views.

Conclusion

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.

Clone this wiki locally