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 emitstrue
;
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 andUISwitch
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 fromstorage
(line 13);.onNext(true)
just before saving change tostorage
(line 16);.onNext(false)
just after saving change tostorage
(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
:
settingSwitch.isOn
property is set with toggle initial value and can be later force-changed with toggle fallback value;settingSwitch.isEnabled
is changed accordingly toToggle
activity;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.