Building Breather (Part 3): Managing networking with RxMoya and handling errors with RxSwift’s retry and materialize

Breather is an open-source iOS app that shows the weather, air pollution and asthma conditions around you.

Alexandros Baramilis
9 min readMay 2, 2019

To raise awareness about air pollution and practice my iOS skills at the same time, I’m building an open-source app where I apply some of the latest knowledge I gain about iOS, Swift and programming. Since there is a lot of information to digest, I’m splitting it into small manageable parts:

Managing networking with Moya/RxMoya

Moya is a network abstraction layer written in Swift. Most iOS developers have probably used Alamofire at one point or another to abstract away all the basic and repetitive network code. Moya, which is actually built on top of Alamofire, takes it one level higher, allowing you to specify whole APIs, or API mirrors as I like to call them, and you’ll soon see why.

If you’re cautious about “yet another new dependency”, Moya has at the time of writing 10,591 stars on Github (vs. 30,758 for Alamofire) and 186 contributors (vs. 175 contributors for Alamofire), so I’d say it’s about as safe a bet as using Alamofire and if you’re already using Alamofire, might as well go the extra mile, because Moya has:

  • Compile-time checking for correct API endpoint accesses.
  • Lets you define a clear usage of different endpoints with associated enum values.
  • Treats test stubs as first-class citizens so unit testing is super-easy.

and also has RxSwift and ReactiveSwift extensions!

But let’s see it in action.

Let’s create a new Networking group in Xcode to separate the networking layer of the application and inside create a new file called AirVisualAPI.

AirVisual API setup with Moya

In Moya, you declare your API as an enum and the endpoints as cases. Simple as that!

You can also include static constants, but maybe not your API keys like I did here :P (read here, here and here if you’re wondering why).

Then you conform to the TargetType protocol, which forces you to specify a baseURL, and by switching through the endpoints, you specify their path, method, headers, sample data (quite handy for unit tests), validation type (if you want to filter return codes) and network task, where you can choose from many default types, include parameters, etc.

For Breather, using Moya might seem like overkill, but for a big project where you might have more than 10 endpoints just for an auth module, it really helps to organise your networking code.

Dispatching the network requests is super simple too.

We’ll be using the RxSwift extensions of Moya, so you need to add the ‘Moya/RxSwift’ pod.

First, we need a MoyaProvider with a Target that conforms to the TargetType protocol, in this case our AirVisualAPI.

Initialising a MoyaProvider with an AirVisualAPI target

(Here, I just instantiate a MoyaProvider and some default values for latitude and longitude upon initialisation of the MainViewModel, but later on we’ll inject these properly.)

Then we just call the request method of RxMoya and specify our endpoint and parameters.

RxMoya’s request-making method

This method returns a Single<Response>, where Response is Moya’s response object. It’s important to remember here, that since we’re in the reactive world, the request won’t be dispatched until someone subscribes to it.

One option would be to directly subscribe to it like:

Subscribing to a Moya request

But in the context of Breather, we want to fire the request whenever the user refreshes the UI and the viewDidRefreshSubject emits an event, so we flatMap the request and make it a part of our observable chain (taking the place of delay and map in Part 2 — Bonus, where we were mocking the request).

Dispatching a network request with RxMoya as part of our observable chain

You can run the app now and if you have internet connection, the console should log:

response: Status Code: 200, Data Length: 352

Handling errors with RxSwift’s retry and materialize

But what if you go offline for a while, or another error occurs? The observable chain will error out and get disposed and you won’t be able to refresh again! 🤭

Thankfully, RxSwift has many handy operators to bounce back from these errors.

1. The retry operator

One option to recover from errors is to use RxSwift’s retry operator. This makes you resubscribe to the observable whenever it emits an error and repeats the sequence until it successfully terminates.

RxSwift offers two versions of retry:

  • retry(): retries unlimited times
  • retry(maxAttemptCount: Int): retries a specified amount of times

You can either use it at the end of the chain, before subscribe:

Retry the whole chain until it successfully completes

Or on specific parts of the chain, like our RxMoya request:

Retry the request up to 3 times

If you want to use the unlimited retry operator, you have to use asObservable() to convert the Single to an Observable.

Retry the request unlimited times

If you want even more customisation, you can use RxSwiftExt’s retry, which has four behaviours with various predicate and delay options: immediate, delayed, exponentialDelayed andcustomTimerDelayed.

A good use-case would be in the case of the network request, where it doesn’t make sense to attempt thousands of requests per second if you’re offline. You can instead try one request per second and you can even set a maximum amount of times to try with that delay, so you can show an error after a certain amount of time.

Retry is a powerful operator that can help you bounce back from errors with no sweat in many cases, but at some point errors need to happen and you need to handle them. That doesn’t mean you can’t combine it with other error handling solutions, like in the above case where you retry a network request and if it keeps failing, you move on to handle the error. However, for simplicity’s sake, in Breather, I will handle the errors directly.

2. The materialize operator

I spent hours and hours investigating various error handling solutions, including making my own custom and generic RxSwift operators, wrapping events in Result, etc, but I kept coming back to materialize. It can appear tricky in the beginning but if you learn how to use it properly it can be quite elegant and powerful and saves you from many messy solutions.

What materialize does is it converts any Observable into an Observable of its events. This means that all next, error and completed events will be emitted as next events wrapped in an event object.

Using materialize at the end of the chain

If we use materialize at the end of the chain and we observe a next event, the console will log:

event: next(Status Code: 200, Data Length: 354)

Remember, before we had:

response: Status Code: 200, Data Length: 352

So you can see that with materialize it is wrapped as a next event.

(I also changed the parameter name and print statement from response to event to reflect that we’re getting events inside onNext and not the response)

If we observe an error, the console will log:

event: error(…lots of error messages…)

If you look in the above code, in subscribe, we’re only printing next events. So this is a next event with an error event element!

If you’re running the code, you will also notice that the sequence terminated. This is because, even though materialize wraps all events as next event, it will also terminate if it gets an error or completed event.

So for our purpose here — to catch the error and handle it before it terminates the chain — we can’t use materialize on the main chain.

But we can use it inside the flatMap for our request. Materialize only works on Observable so we also need to use asObservable() to convert the Single.

Using materialize on the RxMoya request

Now, when we observe a next event, the console will log:

event: next(Status Code: 200, Data Length: 356)

event: completed

This is because Single emits a success or an error and then completes, but because we used materialize, we get both the next and the completed event as next events. This means that the sequence doesn’t terminate and we can keep firing away!

If we get an error (btw I’m simulating errors by turning off the Wi-Fi), the console logs:

event: error(…lots of error messages…)

But you will notice that this time you can keep retrying by pulling down to refresh and you keep getting errors without terminating the sequence.

And if you turn on Wi-Fi and pull again, you get another next and completed event!

Handling the errors and showing messages to the user

Ok so we caught the errors and prevented the sequence from terminating, so the user can keep refreshing the UI, but we also need to propagate the errors to the user.

We can add another output to our Output struct called error, of type Driver<Error> and a corresponding errorSubject of type PublishSubject<Error>.

The error Driver for emitting errors to the view controller

Initialise it when we’re initialising the output:

Initialising the error Driver

We have to specify a default error to return so I’m using a handy enum that I made for managing errors. It conforms to the Error protocol and via extension to the LocalizedError protocol that requires it to specify an errorDescription var. It’s in the file called Errors.swift in the Constants group under Supporting Files. I will also be expanding on this enum with more errors in the next parts.

The BreatherError enum

Now we need to emit an event on the errorSubject when we catch an error.

We modify the subscribe handler for onNext with a switch, to differentiate between next events that contain actual elements and next events that contain errors.

Switching between next events with elements or errors

If we get an error, we emit it through the errorSubject.

The last thing we need to do is bind this output in our bindViewModel method in MainViewController.

Binding the error output

If you’re wondering what showAlert is, it’s a little extension I made on UIViewController, that abstracts away all the verbose code of showing an alert. You just need to pass a title and a message, in this case a default “Error” title for all errors and the localizedDescription of the error, which is a user friendly error message.

UIViewController extension for presenting alerts

And now we’ve gracefully handled errors. You can retry as many times as you like, by pulling down to refresh or by pressing on the home button and then bringing the app back and you will get this alert every time if you’re offline. When you go back online, the app will recover automatically and you can keep refreshing normally :)

Presenting the alert to the user

Keep reading → Part 3 (Bonus): Combining network requests with RxSwift’s zip and switching tuples with Swift’s switch.

--

--