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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
RxSwift: Transforming Operators in Practice
35 mins
- Getting Started With GitFeed
- Fetching Data From the Web
- Using map to Build a Request
- Using flatMap to Wait for a Web Response
- share vs. shareReplay
- Transforming the Response
- Processing the Response
- Intermission: Handling Erroneous Input
- Persisting Objects to Disk
- Add a Last-Modified Header to the Request
- Challenges
- Where to Go From Here?
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.
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.