RxSwift: Transforming Operators in Practice

Learn how to work with transforming operators in RxSwift, in the context of a real app, in this tutorial taken from our latest book, RxSwift: Reactive Programming With Swift! By Marin Todorov.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

Processing the Response

Yes, it’s finally time to perform some side effects. You started with a simple string, built a web request, sent it off to GitHub, and received an answer back. You transformed the response to JSON and then to native Swift objects. Now it’s time to show the user what you’ve been cooking up behind the scenes all this time.

Add this code anywhere in ActivityController’s body:

func processEvents(_ newEvents: [Event]) {

}

In processEvents(_:), you grab the last 50 events from the repository’s event list and store the list into the Variable property events on your view controller. You’ll do that manually for now, since you haven’t yet learned how to directly bind sequences to variables or subjects.

Insert into processEvents():

var updatedEvents = newEvents + events.value
if updatedEvents.count > 50 {
  updatedEvents = Array<Event>(updatedEvents.prefix(upTo: 50))
}

events.value = updatedEvents

You append the newly fetched events to the list in events.value. Additionally, you cap the list to 50 objects. This way you will show only the latest activity in the table view.

Finally, you set the value of events and are ready to update the UI. Since the data source code is already included in ActivityController, you simply reload the table view to display the new data. To the end of the processEvents function, add the following line:

tableView.reloadData()

Run the app, and you should see the latest activity from GitHub. Yours will be different, depending on the current state of the repo in GitHub.

Note: Since you are currently not managing threads, it might take a while for the results to show up in the table. That’s because you end up updating your UI from a background thread. Although this is a bad practice, it still happens to work on the current version of iOS. For now, ignore the delay and you will fix your code later in the tutorial. While waiting, click on the simulator to force a refresh.

Since the code that came with the starter project in viewDidLoad() sets up a table refresh control, you can try to pull down the table. As soon as you pull far enough, the refresh control calls the refresh() method and reloads the events.

If someone forked or liked the repo since the last time you fetched the repo’s events, you will see new cells appear on top.

There is a little issue when you pull down the table view: the refresh control never disappears, even if your app has finished fetching data from the API. To hide it when you’ve finished fetching events, add the following code just below tableView.reloadData():

refreshControl?.endRefreshing()

endRefreshing() will hide the refresh control and reset the table view to its default state.

So far, you should have a good grasp of how and when to use map and flatMap. Throughout the rest of the tutorial, you are going to tie off a few loose ends of the GitFeed project to make it more complete.

Intermission: Handling Erroneous Input

The project as-is is pretty solid, at least in the perfect safety of a Swift Playground or in a step-by-step tutorial like this one. In this short intermission, you are going to look into some real-life server woes that your app might experience.

Switch to Event.swift and have a look at its init. What would happen if one of those objects coming from the server contained a key with a wrong name? Yes you guessed it — your app would crash. The code of the Event class is written somewhat lazily, and it assumes the server will always return valid JSON.

Fix this quickly before moving on. First of all, you need to change the init to a failing initializer. Add a question mark right after the word init like so:

init?(dictionary: AnyDict)

This way, you can return nil from the initializer instead of crashing the app. Find the line fatalError() and replace it with the following:

return nil

As soon as you do that, you will see a few errors pop up in Xcode. The compiler complains that your subscription in ActivityController expects [Event], but receives an [Event?] instead. Since some of the conversions from JSON to an Event object might fail, the result has now changed type to [Event?].

Fear not! This is a perfect opportunity to exercise the difference between map and flatMap one more time. In ActivityController, you are currently converting JSON objects to events via map(Event.init). The shortcoming of this approach is that you can’t filter out nil elements and change the result, so to say, in mid-flight.

What you want to do is filter out any calls to Event.init that returned nil. Luckily, there’s a function that can do this for you: flatMap — specifically, the flatMap on Array (not Observable).

Return to ActivityController.swift and scroll to fetchEvents(repo:). Replace .map(Event.init) with:

objects.flatMap(Event.init)

To recap: any Event.init calls will return nil, and flatMap on those objects will remove any nil values, so you end up with an Observable that returns an array of Event objects (non-optional!). And since you removed the call to fatalError() in the Event.init function, your code is now safer. :]

Persisting Objects to Disk

In this section, you are going to work on the subplot as described in the introduction, where you will persist objects to disk, so when the user opens the app they will instantly see the events you last fetched.

In this example, you are about to persist the events to a .plist file. The amount of objects you are about to store is small, so a .plist file will suffice for now.

First, add a new property to the ActivityController class:

private let eventsFileURL = cachedFileURL("events.plist")

eventsFileURL is the file URL where you will store the events file on your device’s disk. It’s time to implement the cachedFileURL function to grab a URL to where you can read and write files. Add this outside the definition of the view controller class:

func cachedFileURL(_ fileName: String) -> URL {
  return FileManager.default
    .urls(for: .cachesDirectory, in: .allDomainsMask)
    .first!
    .appendingPathComponent(fileName)
}

Add that function anywhere in the controller file. Now, scroll down to processEvents(_:) and append this to the bottom:

let eventsArray = updatedEvents.map{ $0.dictionary } as NSArray
eventsArray.write(to: eventsFileURL, atomically: true)

In this code, you convert updatedEvents to JSON objects (a format also good for saving in a .plist file) and store them in eventsArray, which is an instance of NSArray. Unlike a native Swift array, NSArray features a very simple and straight-forward method to save its contents straight to a file.

To save the array, you call write(to:atomically:) and give it the URL of the file where you want to create the file (or overwrite an existing one).

Cool! processEvents(_:) is the place to perform side effects, so writing the events to disk in that place feels right. But where can you add the code to read the saved events from disk?

Since you need to read the objects back from the file just once, you can do that in viewDidLoad(). This is where you will check if there’s a file with stored events, and if so, load its contents into events.

Scroll up to viewDidLoad() and add this just above the call to refresh():

let eventsArray = (NSArray(contentsOf: eventsFileURL) 
  as? [[String: Any]]) ?? []
events.value = eventsArray.flatMap(Event.init)

This code works similarly to the one you used to save the objects to disk —  but in reverse. You first create an NSArray by using init(contentsOf:), which tries to load list of objects from a plist file and cast it as Array.

Then you do a little dance by using flatMap to convert the JSON to Event objects and filter out any failing ones. Even though you persisted them to disk, they all should be valid, but hey — safety first! :]

That should do it. Delete the app from the Simulator, or from your device if you’re working there. Then run the app, wait until it displays the list of events, and then stop it from Xcode. Run the project a second time, and observe how the table view instantly displays the older data while the app fetches the latest events from the web.