RxSwift and Production-Level Code

RxSwift and Production-Level CodePlus a few other things I wish someone had told me when I started using RxSwiftMichael LongBlockedUnblockFollowFollowingMay 27Visit the RxSwift repository on GitHub and you’ll find a classic example of the power of RxSwift: the GitHub Search.

In just 18 lines of code you have implemented a full blown user interface, binding some search bar text to a data query, and then binding the result of the data query directly to a table view.

Powerful stuff, indeed.

But, as mentioned in an earlier article, RxSwift: The Complexity Tradeoff, this sort of sample code makes some distinct tradeoffs for the sake of brevity and clarity and is far from production ready.

What is production-level code?For starters, there’s no real error handling code.

Any API errors are swallowed and the user is left with an empty search screen.

This is far from ideal.

And surely we would want to indicate to the user that a search is in progress?Speaking of searching, adding a search cache would be nice.

Why hit the API again for results we’ve already seen?Finally, the search code as presented appears to be embedded directly in the view controller.

That’s not good — that type of logic should really be in a ViewModel (for MVVM, at least.

)Then again, supporting all of those things would add much complexity to the code, make it harder to understand, and reduce many of the advantages of using RxSwift…wouldn’t it?Let’s find out.

Along the way, I’ll demonstrate some RxSwift best practices, tips, and design patterns — things I wish someone had told me when I started using RxSwift.

The GitHub search ViewModelLet’s start by creating a ViewModel that implements our search logic.

As per our revised requirements, it accepts a search string and then presents our search results (or an error message, should an error occur).

It looks like this:Note that our data and errors are exposed as simple observables and not as subjects.

This ensures that only our view model controls any events that our view controller might see.

Further, our only input comes from the search text passed as an observable parameter to our configure function.

This simplifies the code and debugging, — we now know there’s just one place where events can hit our view model.

For example, a separate and distinct load() function, could be called by anyone at any time, increasing the chances of unexpected results.

Finally, we’ve also added a variable for a GithubSearchService that, like the searchGitHub(query) function in the original example, will handle our actual URLSession data task request.

It was a blind function there, so we’ll treat it as such.

When all is said and done, defining a basic ViewModel adds just seven lines of code.

Our configuration functionThe configuration function is where most of the magic happens, so let’s implement it.

It contains the bulk of the original example code, plus a bit more to improve our error handling.

Error handlingIf you’ve done much RxSwift, or if you’ve read RxSwift: Better Error Handling With CompactMap, the pattern above is clear.

We wrap our asynchronous search call inside a flatMapLatest function and split the results of that function into two streams — one for our data and one for our error.

The result of our flatMap is an Observable<Event<[Repository]>>.

We’re going to use RxSwift’s materialize function to turn our data and any potential errors into a stream of events.

This function takes the place of the simple .

catchErrorJustReturn([]) line in the original code.

If the search operation succeeds, our result is of type Event.

next<[String]>.

If an error, it’s Event.

error(Swift.

Error).

In either case, we’ve protected our observable stream from any errors escaping the flatMap and terminating our event stream.

(If this isn’t clear, the CompactMap article explains this using materialize in more detail.

)Moving on, the assignment to data uses the Event’s element variable to extract the optional data from the event.

If none, we return an empty array.

Similarly, the assignment to error extracts the error, if any.

If no error occurred, the optional unwrap will be nil, and we’ll pass that along so any previous errors will be cleared automatically.

The error result is also shared() as we’ll be using the output of error a couple of times in our view controller.

There is no point in executing this code more than necessary.

Net result: the observable and five for the data/error split.

Activity indicatorAs mentioned in our requirements, we want to indicate to the user that a search is in progress.

If you think that means we need another observable to show that we’re processing a request, you’d be correct!public var processing: Observable<Bool>!To feed it, we need to add just two lines of code to the end of our configuration function.

processing = Observable<Bool> .

merge( search.

map { _ in true }, data.

map { _ in false } )Here we take our original search event, map it to true, and use that to trigger our spinner.

And when we see an event on result, empty or otherwise, we map it to false and use it to stop our spinner from spinning.

Both events are merged and sent down the line via our single exposed processing flag .

You’ll see this multiple-map, merge-events idiom used quite a bit in RxSwift code.

Three more lines added.

CachingThe last thing we wanted to add from our revised list of requirements was some form of search result caching, so we didn’t hit our API quite as often.

Caching needs a cache, so let’s get add one.

We’re going to just use a simple dictionary with the search key as a lookup value:internal var searchCache = [String:[Repository]]()We’ll handle the implementation inside our flatMapLatest closure:Straightforward enough.

If our cache contains results from our original search key, we just return them before we attempt our API call.

If not, we do the search and add a do(onNext: { … )) handler to capture the result and add it to our cache.

Should the API call error out, the do function is bypassed.

Should you require a more robust caching mechanism, you’re free to add one.

Just insert your cache request and cache insert function calls into the code where we just added ours.

That’s it, just eight more lines of code, and the most we’ve needed for one of our new features.

ViewModel final resultIf you’ve been keeping track, our completed view model should look like this:Take a moment to also note how our observable’s stay “in-stream”, passing from the original event through our processing code and directly into our output streams.

This is definitely a RxSwift Best Practice.

More code?More code than the original?.Of course.

Almost twice as much, in fact.

But we have added better error handling, a processing flag, and implemented API caching at the same time.

Nor am I above mentioning that about quarter of the code added was that needed to move the original code into a view model — a wash no matter what the methodology.

The GitHub search ViewControllerSince we moved most of the business logic out to the view model, our view controller should be fairly simple.

And it is.

Let’s look at the shell:We have our view model and we instantiate a RxSwift DisposeBag.

We also have outlets for the search bar, our result table view, a label for the error message, and an outlet for the activity indicator.

The viewDidLoad() function calls super and then calls two functions, one to configure the view model and and another to bind our observables.

Note that the class definition, view model definition, and outlets would be required for any implementation of the functionality needed, RxSwift or otherwise.

configureViewModel()Let’s start by defining our configuration function:We make an observable of our search bar text and use orEmpty to make it a String should the value be nil.

The throttle function waits a half second before sending the event on, effectively delaying the search until the user stops typing.

Then distinctUntilChanged() makes sure the final value is in fact different from the previous value before we go bother our view model with it.

We then pass our text observable to our view model.

Fini.

setupObservables()This is the heart of the matter.

The spot that ties together all our observables to our various views — the place where we feed our tableview.

It has to be complex, right?You be the judge:We bind our repository data to our table view, configuring the cell.

We bind the error message to the error label and map the same value to the isHidden flag, so the error label shows and hides appropriately.

And we bind the processing flag to our spinner.

Note here that we’re using the variadic disposeBag insert method to clean up the code.

And… that’s it.

No tableView delegates.

No tableView numberOfRows or cellForRowAtIndexPath functions.

Our final view controller source code is as follows:Completion handlerSo we’ve taken the classic RxSwift GitHub search demonstration code and converted it to modern, MVVM, production ready code.

Yes, the code is a bit longer, but it does more and is more robust to boot.

And again, I’d like to stress that a good portion of the longer code is boilerplate class definitions and UIView outlets — things that are pretty much required no matter what implemention methodology you choose, RxSwift or otherwise.

Some common RxSwift best practices were also demonstrated, including “in-stream” input/output observable streams, the use of temporary scoped observable streams, and a couple of the techniques used to split input streams into multiple output streams.

Plus we looked at some of the thought processes involved along the way.

As always, if you have questions just leave them in the comment section below and I’ll do my best to answer them.

And if you’d like to see more of these “follow along with the code” articles let me know that too, or just give me a thumbs up if you’re busy.

Til next time.

.

. More details

Leave a Reply