Toggle Component with RxSwift

List of toggle switches is widely present in mobile world. I find it in “setting” screens of most apps that I use. For me, this simple act of switching things on and off plus saving it somewhere hides interesting complexity that I’m going to reveal.

In this article I will build “toggles list” screen with RxSwift. I’m going to use technique introduced in my previous article about Autocomplete. We will create Toggle component — a black-box with inputs and outputs to plug. After this reading you’ll be able to use it in your project to build your own toggling experience.

Toggle

First, see the experience we’re going to build. Notice that single toggle is disabled for the time of sending its status to server and that sometimes it shakes and falls back to the previous state — when saving error occurs:

On the code side, one instance of Toggle manages single UISwitch. Toggle uses internal storage for persisting the on/off value. Normally, the storage would send it to the server, but the one we will build only simulates network call and keeps the value in memory. What’s more, every 3 saves an error occurs — it’s made to demonstrate how the value is rolled back with shake animation. Finally, UISwitch gets disabled for the time of network operation being done.

The public interface of Toggle is this:

The same in Swift:

Toggle has one input:

  • change: Observable<Bool> — emits change in UI. When toggle switch is set off and user turns it on, it emits true;

and two outputs:

  • value: Driver<ToggleValue> — when subscription is made, it emits .initial(Bool) or .unknown(Error) depending if the toggle’s initial value was successfully read from storage. When user switches toggle’s UI, it emits .updated(Bool) if new value was successfully saved or .fallback(Bool) if not. In our case, .updated(Bool) means that the change was saved and UISwitch is synchronized with the storage. .fallback(Bool) means that we need to cancel the change to stay in sync.
  • isBusy: Driver<Bool> — it indicates either asynchronous save or async read operation being done by storage.

Later we will mix this inputs and outputs together. Let’s start by creating the storage.

Toggle Storage

Storage provides an interface for saving and reading Bool value:

Both methods return Observable meaning that the result will arrive in the future. If an error occurs, .error() event is emitted.

Depending on your need, save(value:) and read() might store the value on server, keep in memory or simply persist in UserDefaults. Like said, we choose the first option, but instead of doing real API call, we’re going to fake it with using .delay() operator. This is the storage we will use:

When reading from NetworkToggleStorage, internall value is emitted with 3 seconds delay. When saving it waits 1 second, then emits .next() or .error() accordingly to modulo 3 heuristic given in shuoldFail().

Notice that ToggleStorage doesn’t know about ToggleValue from Toggle interface. Instead, it uses Bool in save(value:) and read(). It will be Toggle’s job to wrap this Bool in ToggleValue context.

Wrap it up

Given ToggleStorage and Toggle, we can define initializer to inject the first into latter:

Regarding what we said about the first output, value: Driver<ToggleValue> emits in two circumstances:

  • after subscription is made, it emits initial value read from storage;
  • after user taps the switch, it emits value saved to storage.

In both conditions the storage operation can succeed or fail. This translates into four possible values of ToggleValue:

We will consider both scenarios of emitting ToggleValue on value: Driver<ToggleValue> output separately.

“Value” Output — getting initial value

Before we present toggle’s UI control on screen, we must know its default value. We’re going to read it from storage just after manage(value:) is called:

We simply map the value to ToggleValue.initial($0). If error occurs, we don’t want the .error() event to be send in initialValue stream. Instead, we want to catch the error and send regular ToggleValue.unknown($0) event on original stream. This happens in line 7 with .catchError operator.

“Value” Output — getting updated value

Once UI is presented, user taps on toggle switch creating .next(Bool) event on change: Observable<Bool> input stream. We need a stream that saves this change to storage:

Whenever we need to do asynchronous operation in a stream, .flatMap is a good choice. It will execute nested stream and emit its events on caller stream. In our case, this nested stream is composed of save result mapped to ToggleValue.update (lines 9, 10). If saving fails, we catch the error and turn it into ToggleValue.fallback (line 11). There is a trick: we ignore the error information and associate negation of valueToSave with .fallback case. Because the requested change was not saved to storage, we want the UI to be synchronized with the previous value.

Given both streams: initialValue and valueAfterSaving, all we need to do is to merge both to fulfill output contract:

We cast resultant stream asDriver. Providing recovery value in this case is only an API requirement, because in lines 7 and 14 we ensure that neither of this two streams will result with error.

"Is Busy" Output

The isBusy: Driver<Bool> output should emit true just before anystorage operation is started and mark its completion by emitting false:

This solution is similar to what we did in Autocomplete component. We use PublishSubject<Bool> and send .next() event in appropriate moments:

  • .onNext(false) when it received initial value from storage (line 13);
  • .onNext(true) just before saving change to storage (line 16);
  • .onNext(false) just after saving change to storage (line 16).

To indicate beginning of initial value loading, we start isBusy stream with emitting true (line 7).

I believe that using PublishSubject makes this code simple and readable. It’s possible to avoid side-effects and get rid of subject by defining pure streams. I did this exercise in Autocomplete article by doing the same implementation with and without subject.

Building UI

The Toggle component is ready ✅. Now it’s time to use it with some UI. Here I put 6 UISwitches and UILabels in stack view:

and provide scaffold for configuring single UISwitch with Toggle:

Now, outputs orchestration will be done in setUp(settingSwitch:with toggle:). It takes settingSwitch and toggle as arguments. In the video above there are three types of modifications applied to UISwitch:

  1. settingSwitch.isOn property is set with toggle initial value and can be later force-changed with toggle fallback value;
  2. settingSwitch.isEnabled is changed accordingly to Toggle activity;
  3. settingsSwitch.shake() animation is played for toggle fallback value.

The first modification is defined by initialValue and fallbackValue streams merged into single switchValue stream:

The switchValue stream drives rx.isOn property of settingSwitch. Although .flatMap { $0.map(Driver.just) ?? Driver.empty() } might look mysteriously, it emits value only if it’s not nil. It’s equal to .unwrap() if you use RxSwiftExt library.

For code readability we use this three convenience properties fromToggleValue extension:

The second mutation is straightforward:

It negates isBusy output to drive settingSwitch.rx.isEnabled.

The third mutation plays .shake() animation when fallback value appears on fallbackValue stream:

We don’t care of the value emitted on fallbackValue, so we map it to void (). Finally, the shake() animation is made by this extension of UISwitch:

It’s worth noting that Toggle component doesn’t limit us to build this single type of UX. Some apps block entire screen for the time of saving setting on server. This can be easily done with Toggle by simply merging multiple isBusy outputs into single isLockViewVisible stream.

Unit Tests

Because the only Toggle’s dependency is ToggleStorage which is a protocol, it’s very easy to create MockToggleStorage and fully test Toggle. One good example is this test ensuring that update values are emitted after user input.

We use helper recordedValues array to store all values emitted by value output. Because the mock storage is asynchronous, we wait for the initial value in line 19. Lines 21 and 22 simulate user input. Line 24 waits until ToggleValue.updated is emitted.

All tests can be found in project’s repository.


Get more

Go to Rx-Toggle repository on GitHub to find the full source code for the work done in this article.

If you enjoyed it, follow me on Twitter to stay tuned — I’m going to continue this series.