Building Breather (Part 2 — Bonus): Refreshing the UI with loading state and mocking API requests with delay

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

Alexandros Baramilis
6 min readApr 27, 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:

Pull to refresh with UIRefreshControl of UIScrollView

It has become common for apps to have this pull down to refresh feature and Apple has made it easy to add this functionality with UIRefreshControl. It can be added to any UIScrollView object or subclass, such as UITableView and UICollectionView.

In Breather, we already have a scroll view, so we just need to pull an IBOutlet into MainViewController:

Then we add a UIRefreshControl object to our private properties:

Now I’m going to extend this example a little bit because I want the view to request fresh data at 3 points:

  1. When we open the app for the first time.
  2. When we bring the app from the background to the foreground.
  3. When the user pulls down to refresh.

For 1: we call refresh() in viewDidLoad.

For 2: we add an observer for the UIApplication.willEnterForegroundNotification event and in the willEnterForeground method we call refresh() again.

For 3: in viewDidLoad we called setupRefreshControl(), which adds a target to our refreshControl object for when the .valueChanged event and calls the refresh selector, which is our refresh() method. This is why I marked it with @objc in the first place.

So now at any of the three cases above, refresh() will be called, which emits a next event on the refreshSubject which is bound to viewModel.input.viewDidRefresh and prompts the view model to refresh the data.

In setupRefreshControl() we also need to add the refreshControl object to the refreshControl property of the scrollView.

Now if you run the app and pull down, you will get the refresh indicator.

But how do you stop it? It’s as simple as calling refreshControl.endRefreshing().

However, we need to call it at the appropriate time.

Getting the UI loading state from the view model

The view model determines the loading state because it is the one who’s in charge of getting the data and feeding it to the view controller.

We can add an isLoading Driver<Bool> property to our Output struct in MainViewModel and a private isLoadingSubject PublishSubject<Bool>.

I wrote more about this pattern in Part 2, but basically anyone can subscribe to isLoading to get the loading state, but only the view model can push events to the loading subject.

In init(), we also need to add the isLoading initialisation when we initialise the output:

This binds the observable side of isLoadingSubject to the isLoading output as a Driver. We use Driver because we’re working directly with the UI and Driver is always observed on the main scheduler and never errors out.

Now, we can go back to MainViewController and bind our new output. In bindViewModel(), we add the following code under the outputs.

Whenever we receive a next event from the isLoading output of the view model, we will call showLoadingIndicators, passing the element, which is a Bool.

This function will toggle both the network activity indicator (the little loading element you get on the status bar) and if force is false, it will end the refreshing of the refreshControl.

Mocking API requests with delay and toggling loading state

Finally, to put everything together, we will mock an API request with delay, so we can observe the loading state.

In Part 2, whenever we observed a next event on the viewDidRefresh input (which is bound to the viewDidRefreshSubject), we mapped some sample data and pushed it with a next event on the cityConditionsSubject (which all the outputs are observing to transform and emit their data).

What we had before (in Part 2)

Now, at the beginning of the chain, we can emit a next event on the isLoadingSubject with a true element, to indicate that the loading has started.

Then we chain the delay operator with an RxTimeInterval(3) on the main scheduler. This will shift the emission of all elements by 3 seconds.

After the delay and map (which is basically our mocking of the request and response), we emit another next event on the isLoadingSubject with a false element, to indicate that loading has finished.

What we have now

Now we can observe all 3 refreshing actions:

  1. When you open the app for the first time, you are met with the “No data” UI and the network activity indicator on the status bar.
Opening the app for the first time

After 3 seconds, the “loading” is finished and you can see the UI populated with the sample data.

UI after loading sample data

2. If you tap the Home button and send the app to the background and then tap on the app to bring it up again, you will see the network indicator on the status bar, meaning that the app is loading. After 3 seconds it will stop. You won’t observe any changes in the data since we’re loading the same sample data again.

Loading after bringing the app to the foreground

3. If you pull down to refresh, you can see both the network activity indicator and the refresh control. After 3 seconds, they should both disappear.

Pull down to refresh

And that’s it! Now if you need to refresh the UI at any other point in the code, you only need to call refresh().

EDIT: I replaced the willEnterForeground notification with didBecomeActive notification.

This has two advantages:

  • It takes care of two of the above three cases: when the app loads for the first time and when the app returns to the foreground. So we don’t need to call refresh() in viewDidLoad anymore.
  • It avoids the “Software caused connection abort” bug that aborts network requests when we bring the app to the foreground. It doesn’t matter in this part because we’re not doing network requests, but it will matter later. If you’re interested about the bug you can read more about it here.
Replaced willEnterForeground with didBecomeActive

--

--