Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

10. Combining Operators in Practice
Written by Florent Pillet

In the previous chapter, you learned about combining operators and worked through increasingly more detailed exercises on some rather mind-bending concepts. Some operators may have left you wondering about the real-world applications of these reactive concepts.

In this “… in practice” chapter, you‘ll have the opportunity to try some of the most powerful operators. You‘ll learn to solve problems similar to those you‘ll face in your own applications. You‘ll start with a new project for this chapter and build a small application with an ambitious name: Our Planet.

Note: This chapter assumes you’ve already worked your way through Chapter 9, “Combining Operators.” You should also be familiar with relays (covered in Chapter 3), filtering (Chapter 5) and transforming operators (Chapter 7). At this point in the book, it is important that you are familiar with these concepts, so make sure to review these chapters if necessary!

Getting started

The project will tap into the wealth of public data exposed by NASA. You‘ll target EONET, NASA’s Earth Observatory Natural Event Tracker. It is a near real-time, curated repository of natural events of all types occurring on the planet. Check out https://eonet.sci.gsfc.nasa.gov/ to learn more!

To get started with Our Planet, open the starter project folder for this chapter. Install the required CocoaPods (as explained in Chapter 1, “Hello RxSwift”), and open OurPlanet.xcworkspace.

Build and run the starter application; the default screen is an empty table view.

Your tasks with this application are as follows:

  • Gather the event categories from the EONET public API https://eonet.sci.gsfc.nasa.gov/docs/v2.1 and display them on the first screen.
  • Download events and show a count for each category.
  • When your user taps a category, display a list of events for it.

You’ll learn how useful combineLatest can be in several situations, but you’ll also exercise startWith, concat, merge, reduce and scan. Of course, you’ll also rely on operators you are already familiar with, like map(_:) and flatMap(_:).

Preparing the web backend service

Good applications have a clear architecture with well-defined roles. The code that talks with the EONET API shouldn’t live in any of the view controllers. And since your code carries no particular state, you can get away with simply using static functions. For clarity, you’ll put the static functions in a class.

Let’s call this the EONET service. It abstracts access to the data exposed by the EONET servers, providing them as a service to your application. You’ll see that, combined with Rx, this pattern will find many applications. It lets you cleanly separate data production from consumption inside your application. You can easily replace or mock the production part, without any impact on the consumption side.

Expand the Model group in the OurPlanet project; the service data structures are ready for you to use. You’ll find EOCategory and EOEvent structures that map to the content delivered by the API.

Open Model/EONET.swift; it’s already been fleshed out with the basic structure of the class, as well as API URLs and endpoints. It also provides a couple of helper functions you’ll use later.

All EONET service APIs use a similar structure. You’ll set up a general request mechanism to get data from EONET and reuse it to read both categories and events.

Generic request technique

You’ll start by coding request(endpoint:query:contentIdentifier:). Your goals with this crucial component of your EONET service are:

  • Request data from the EONET API.
  • Decode the response to a generic dictionary.
  • Make sure all errors are taken care of.

It’s always important to cover error cases. Don’t let errors go silent, unless they’re truly harmless! You want to handle programmer errors (yes, you’ll make some), network errors and content errors.

Let’s get started. Create a new request(endpoint:query:contentIdentifier:) method:

static func request<T: Decodable>(endpoint: String,
                                  query: [String: Any] = [:],
                                  contentIdentifier: String) -> Observable<T> {
  do {
    guard let url = URL(string: API)?.appendingPathComponent(endpoint),
          var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
      throw EOError.invalidURL(endpoint)
    }

Your parameters here are the endpoint name and optional query parameters. The contentIdentifier names the object in the returned data that contains the actual array of objects you‘re interested in. If the URL can’t be constructed (i.e. you changed the service URL and mistyped it), it will throw an error. You’ll catch all future errors in this function.

Once you have the URL, add the query parameters. You will use them later for event requests:

components.queryItems = try query.compactMap { (key, value) in
  guard let v = value as? CustomStringConvertible else {
    throw EOError.invalidParameter(key, value)
  }
  return URLQueryItem(name: key, value: v.description)
}
guard let finalURL = components.url else {
  throw EOError.invalidURL(endpoint)
}

The core processing part of this function uses an RxCocoa extension to URLSession. You learned about rx.response in Chapter 8, and will learn more about RxCocoa in Chapters 12 and 13.

Next, add the following code:

let request = URLRequest(url: finalURL)

return URLSession.shared.rx.response(request: request)
  .map { (result: (response: HTTPURLResponse, data: Data)) -> T in
    let decoder = self.jsonDecoder(contentIdentifier: contentIdentifier)
    let envelope = try decoder.decode(EOEnvelope<T>.self, from: result.data)
    return envelope.content
  }

This is a structure you should now be familiar with. URLSession’s rx.response creates an observable from the result of a request.

Note: When the data comes back, JSONDecoder deserializes it to EOEnvelope, EONET‘s generic data wrapper. The envelope contains the final array of content objects. By the magic of Decodable, these objects are of the type the function specializes to. Learning Swift‘s Codable is a whole topic in itself, but feel free to look into the model objects to understand how it‘s done!

Finally, close the function with a catch statement that simply ignores errors:

  } catch {
    return Observable.empty()
  }
}

Don’t focus on the details of this right now; you’ll learn the details of handling your errors in Chapter 14, “Error Handling in Practice.”

You now have a solid mechanism to perform requests. Next, you need to fetch the event categories.

Fetch categories

To get categories from EONET, you’ll hit the categories API endpoint. Since categories seldom change, you can make them a singleton. But you are fetching them asynchronously, so the best way to expose them is with an Observable<[EOCategory]>.

Add this code to the EONET class:

static var categories: Observable<[EOCategory]> = {
  let request: Observable<[EOCategory]> = EONET.request(endpoint: categoriesEndpoint, contentIdentifier: "categories")

    return request
      .map { categories in categories.sorted { $0.name < $1.name } }
      .catchErrorJustReturn([])
      .share(replay: 1, scope: .forever)
	}()

Here you apply techniques covered in previous chapters:

  • Request data from the categories endpoint.
  • Map the array of EOCategory objects to an array sorted by category name.
  • If a network error occurs at this stage, output an empty array. You’ll learn more about error handling in Chapter 14, “Error Handling in Practice.”

The interesting bit is the .share(replay:scope:) addition at the end. Why would you do this here?

The categories observable you created is a singleton (static var). All subscribers will get the same one. Therefore:

  • The first subscriber triggers the subscription to the request observable.
  • The response maps to an array of categories.
  • share(replay: 1, scope: .forever) relays all elements to the first subscriber.
  • It then replays the last received element to any new subscriber, without re-requesting the data. It acts like a cache. This is the purpose of the .forever lifetime scope.

You’re now ready to wire up the categories view controller!

Categories view controller

The categories view controller presents a sorted list of categories. Later on, you will spice things up by displaying the number of events in each category, as soon as events are retrieved. For now, let’s keep it simple.

Open CategoriesViewController.swift.

You’re displaying a UITableViewController, so you need to store the categories locally for display purposes. Start by adding a BehaviorRelay to hold them (you learned about BehaviorRelay in Chapter 3, “Subjects”). Initial value is an empty array. Subscribing to the relay will trigger an update of the table view every time new data arrives.

Add the relay plus a DisposeBag to hold your subscription disposables inside CategoriesViewController:

let categories = BehaviorRelay<[EOCategory]>(value: [])
let disposeBag = DisposeBag()

To get the number of table view items, pull the current contents from the categories relay. Update the code in tableView(_:numberOfRowsInSection:):

return categories.value.count

Note that you read the current value straight from the categories relay. Later on in the book, you’ll learn about some better techniques using RxCocoa. For now, you’ll keep things simple.

Use the simple default cell to display categories. Insert the following inside tableView(_:cellForRowAt:), just above the return statement:

let category = categories.value[indexPath.row]
cell.textLabel?.text = category.name
cell.detailTextLabel?.text = category.description

You’re done with the basic setup. If you run the application, you won’t see any categories yet, as you first need to subscribe to the observable from the EONET service.

In the empty startDownload() method, add this code:

let eoCategories = EONET.categories
eoCategories
  .bind(to: categories)
  .disposed(by: disposeBag)

Nothing fancy here, since the EONET service is doing all the hard work. bind(to:) connects a source observable (EONET.categories) to an observer (the categories relay).

Finally, subscribe to the BehaviorRelay to update the table view. Add the following code to viewDidLoad() before the line where you call startDownload():

categories
  .asObservable()
  .subscribe(onNext: { [weak self] _ in
    DispatchQueue.main.async {
      self?.tableView?.reloadData()
    }
  })
  .disposed(by: disposeBag)

Note: You’re using a classic DispatchQueue technique to ensure the table view update occurs on the main thread. You’ll learn to use schedulers and the observeOn(_:) operator in Chapter 15, “Intro to Schedulers/Threading in Practice.”

Build and run the application and you’ll see the categories show up.

Now you can move on to downloading the events, where the real Rx fun will happen!

Adding the event download service

The EONET API exposes two endpoints to download the events: all events, and events per category. Each also differentiates between open and closed events.

Open events are ongoing; for example, an ongoing flood or thunderstorm. Closed events have finished and are in the past. The actual EONET request parameters you’re interested in are:

  • The number of days to go back in time to find events.
  • The open or closed status of the events.

The API requires that you download open and closed events separately. Still, you want to make them appear as one flow to subscribers. The initial plan involves making two requests and concatenating their result.

Add a private function to EONET.swift for requesting events with the appropriate parameters:

private static func events(forLast days: Int, closed: Bool) -> Observable<[EOEvent]> {
  let query: [String: Any] = [
    "days": days,
    "status": (closed ? "closed" : "open")
  ]
  let request: Observable<[EOEvent]> = EONET.request(endpoint: eventsEndpoint, query: query, contentIdentifier: "events")
  return request.catchErrorJustReturn([])
}

You’re now familiar with the query model. First you declare the type of the request variable so the compiler knows which type to specialize the request function to. It will automatically decode the JSON contents of the EONET wrapper envelope to an array of EOEvent objects. This time you are passing query parameters to the request(endpoint:query:contentIdentifier) function to get exactly the data you want.

Note: You‘ll learn more about error handling in Chapter 14, “Error Handling in Practice.” Meanwhile, in this small application we simply catch errors and return empty data. More evolved strategies would involve retrying the request, then handling errors at the UI level to alert the user.

Finally, expose a new function in the EONET service to provide an [EOEvent] observable:

static func events(forLast days: Int = 360) -> Observable<[EOEvent]> {
  let openEvents = events(forLast: days, closed: false)
  let closedEvents = events(forLast: days, closed: true)

  return openEvents.concat(closedEvents)
}

This is the function you’ll call from view controllers to get events. Notice the concat(_:) operator? Here’s what’s going on:

This is sequential processing. concat creates an observable that first runs its source observable (openEvents) to completion. It then subscribes to closedEvents and will complete along with it. It relays all events emitted by the first, and then the second observable. If either of those errors out, it immediately relays the error and terminates.

This is a good starter solution, but you’ll improve on it later in this chapter.

You’re now ready to add the events download feature to the categories view controller.

Getting events for categories

Head back to CategoriesViewController.swift. In startDownload(), you’ll need a more elaborate categories download mechanism to download the events. You want to fill up each category with events, but downloading takes time. To provide the best user experience possible, you’ll tackle this as follows:

  • Download categories and display them first.
  • Download all events for the past year.
  • Update the category list to include a count of events in each category.
  • Add a disclosure indicator.
  • Push the events list view controller on selection.

Updating Categories with Events

You first need to replace the code in startDownload() with something more elaborate:

func startDownload() {
  let eoCategories = EONET.categories
  let downloadedEvents = EONET.events(forLast: 360)

}

You start by preparing two observables. eoCategories downloads the array of all categories. The new downloadedEvents calls into the events function you added to the EONET class, and downloads events for the past year.

What you need for this table view now is a list of categories. Peek into the EOCategory model, and you’ll see it has an events property. It’s a var so you can add downloaded events to each category. How are you going to do this?

Add this code at the end of startDownload():

  let updatedCategories = Observable
    .combineLatest(eoCategories, downloadedEvents) {
      (categories, events) -> [EOCategory] in

There you go! You use combineLatest(_:_:resultSelector:) to combine the downloaded categories with the downloaded events and build an updated category list with events added. Your closure gets called with the latest categories array, from the eoCategories observable, and the latest events array, from the downloadedEvents observable. Its role is to combine them and produce an array of categories with their events.

You can now add the guts of the combination closure:

      return categories.map { category in
        var cat = category
        cat.events = events.filter {
          $0.categories.contains(where: { $0.id == category.id })
        }
        return cat
      }
    }

The updatedCategories observable will be of type Observable<[EOCategory]>. This is because the return type of the closure is [EOCategory]. It works with the map operator and lets you create a new Observable type.

The rest of the code above is regular Swift code. Events can belong to several categories, so it walks the category list and adds up all events matching the id.

Finally, bind to the categories relay like so:

eoCategories
  .concat(updatedCategories)
  .bind(to: categories)
  .disposed(by: disposeBag)

This time you use the concat(_:) operator to bind items from the eoCategories observable and items from the updatedCategories observable. This will work just fine because eoCategories emits one element (an array of categories) then completes. This allows the concat(_:) operator to subscribe to the next observable, updatedCategories.

To recap, you’ve rewritten startDownload() to download the events and categories and combine the categories in one observable, with the events in another in order to add the events to the proper category. Now that you have the events for each category, you’ll need to update your user interface to display that information.

Updating the display

Update tableView(_:cellForRowAt:) to display the number of events and a disclosure indicator. Change the cell’s textLabel setup and add the disclosure indicator:

cell.textLabel?.text = "\(category.name) (\(category.events.count))"
cell.accessoryType = (category.events.count > 0) ? .disclosureIndicator : .none

Build and run the application. You should see categories show up with a (0) event counter. After a while (have some patience here, depending on your internet connection), you’ll see counters update with actual events count for the past year, as shown in the example below:

You’ll notice quite a long delay between the time categories appear, and the time they get filled up with events. This is because updates from the EONET API can take some time. After all, you’re requesting a full year of events! What can you do to improve this?

Downloading in parallel

Remember that the EONET API delivers open and closed events separately. Until now, you’ve been using concat(_:) to get them sequentially. It would be a good idea to download them in parallel instead. The cool thing with RxSwift is that you can make this change without any impact on UI code! Since your EONET service class exposes an observable of [EOEvent], it doesn’t matter how many requests your code makes — it’s transparent to the code consuming this observable.

Open the EONET.swift file again, then navigate to events(forLast:). Replace the return statement with the following:

return Observable.of(openEvents, closedEvents)
  .merge()
  .reduce([]) { running, new in
    running + new
  }

What’s happening here?

  • First, you created an observable of observables.
  • Next, you merged them, just as you learned in the previous chapter. Remember, merge() takes an observable of observables. It subscribes to each observable emitted by the source observable and relays all emitted elements.
  • Finally, you reduce the result to an array. You start with an empty array, and each time one of the observables delivers an array of events, your closure gets called. There you add the new array to the existing array and return it. This is your ongoing state that grows until all the observables complete. Once complete, reduce emits a single value (its current state) and completes.

Build and run the application. You may notice a slight improvement in download time, although you’ll soon learn that you can do even better.

Isn’t it cool that you can change processing in your EONET service, without having to touch any of the UI code? This is one of the great benefits of Rx. A clean separation between producer and consumer gives you lots of flexibility.

Events view controller

You can now complete your UI by populating the Events view controller. Not only are you going to display events, but you’ll also wire up a slider to control how much of the past year appears in the list. This is a good occasion to exercise some operators a bit more.

Open EventsViewController.swift and add the following relay to hold the events, as well as the always useful DisposeBag:

let events = BehaviorRelay<[EOEvent]>(value: [])
let disposeBag = DisposeBag()

Note: Tired of adding a DisposeBag everywhere? If your object is a subclass of NSObject (such as your view controllers) there’s hope on the horizon! Look up the NSObject+Rx library on the RxSwiftCommunity GitHub organization. It provides a DisposeBag on demand for any subclass of NSObject!

In viewDidLoad(), add the following code to update the table view every time events gets a new value:

events.asObservable()
  .subscribe(onNext: { [weak self] _ in
    self?.tableView.reloadData()
  })
  .disposed(by: disposeBag)

It would also be wise to ensure the update happens on the main queue, since events may be emitted from a background queue. Unless otherwise specified, subscriptions receive elements on the thread which emitted them. You’ve seen this earlier in this chapter, and you’ll apply the same technique here.

You can now update tableView(_:numberOfRowsInSection:):

return events.value.count

In tableView(_:cellForRowAt:), configure the cell as follows (above the return line at the bottom):

let event = events.value[indexPath.row]
cell.configure(event: event)

Finally, you need to add selection handling to CategoriesViewController. Add the following below tableView(_:cellForRowAt:). This will push your events view controller:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  let category = categories.value[indexPath.row]
  tableView.deselectRow(at: indexPath, animated: true)

  guard !category.events.isEmpty else { return }

  let eventsController = storyboard!.instantiateViewController(withIdentifier: "events") as! EventsViewController
  eventsController.title = category.name
  eventsController.events.accept(category.events)
  navigationController!.pushViewController(eventsController, animated: true)
}

Easy enough; the BehaviorRelay<[EOEvent]> in your Events view controller will hold the events. Setting this relay‘s value automatically triggers an update of the table view. Whether the view is already loaded or not is of no consequence, thanks to observables!

Build and run the application. You can now navigate to the events list of a category:

You’re not done yet! The days selector is not wired up yet — but you’ll see it’s fairly easy to do.

Wiring the days selector

Here’s the general approach you’ll use to wire this one up:

  • Bind the current slider value to a BehaviorRelay<Int>.
  • Combine the slider value with events to make a list of filtered events.
  • Bind the table view to filtered events.

To get started, add the days and filteredEvents relays to EventsViewController:

let days = BehaviorRelay<Int>(value: 360)
let filteredEvents = BehaviorRelay<[EOEvent]>(value: [])

To filter the events, you need to take the latest value of days plus the events and filter them. You want to keep only the last N days you’re interested in. Have you guessed which operator will come to the rescue?

Add this to viewDidLoad():

Observable.combineLatest(days, events) { days, events -> [EOEvent] in
  let maxInterval = TimeInterval(days * 24 * 3600)
    return events.filter { event in
      if let date = event.date {
        return abs(date.timeIntervalSinceNow) < maxInterval
      }
    return true
  }
}

It’s your good friend, combineLatest! You should now recognize the structure of the operator call. You combine the days and events relays. The closure filters out events, keeping only those in the requested days range. Now that you have this observable, you can bind it to the filteredEvents relay by adding:

.bind(to: filteredEvents)
.disposed(by: disposeBag)

Now you need to do two things:

  • Bind the tableView to filteredEvents.
  • Bind the slider to the days value.

The first step is easy. Change events to filteredEvents when subscribing in viewDidLoad() for table view updates:

filteredEvents.asObservable()
  .subscribe(onNext: { _ in
    DispatchQueue.main.async { [weak self] in
      self?.tableView.reloadData()
    }
  })
  .disposed(by: disposeBag)

Scroll down to sliderAction(slider:) — the days slider in the storyboard is already wired to that action method. Insert the following code to update days any time the user moves the slider knob:

days.accept(Int(slider.value))

Finally, update tableView(_:numberOfRowsInSection:) as well to return the number of filtered events instead of counting all of them:

return filteredEvents.value.count

Obviously, you’ll have to also reflect that change in the other data source method as well. Find the line where you fetch the current event in tableView(_:cellForRowAt:) and replace it with:

let event = filteredEvents.value[indexPath.row]

Build and run the application, pick a category with lots of events, then play with the slider. You’ll see the list shorten or lengthen as you drive the slider.

Oh — the label isn’t updating. Add this to viewDidLoad() to fix that:

days.asObservable()
  .subscribe(onNext: { [weak self] days in
    self?.daysLabel.text = "Last \(days) days"
  })
  .disposed(by: disposeBag)

Now your application is complete. Congratulations!

But downloading is still rather slow, and you don’t see much progress while it’s working. You’ll take care of that next!

Splitting event downloads

Your last assignment in this chapter is to split downloads per category. The EONET API lets you either download all events at once, or by category. You’ll download events by category, which will be a bit more complicated due to the simultaneous downloads — but you’re quickly becoming an RxSwift pro and you know you can handle it.

Here’s the strategy you’ll use:

  • First, get the categories.

  • Then, request the events for each category.

  • Each time a new event block arrives, update the categories and refresh the table view.

  • Continue until you’ve obtained events for all categories.

You’ll have to make some changes to CategoriesViewController and to the EONET service. Move to EONET.swift first.

Adding per-category event downloads to EONET

To download events by category, you’ll need to be able to specify the endpoint to use on the API. Update the private events(forLast:closed:) method signature and the first line of code to take the endpoint as a parameter:

private static func events(forLast days: Int, closed: Bool, endpoint: String) -> Observable<[EOEvent]> {

To reflect the endpoint name addition, update the call to the request function a little further down:

let request: Observable<[EOEvent]> = EONET.request(endpoint: endpoint, query: query, contentIdentifier: "events")

Now update the signature of the public events(forLast:) method. Change it to take a second parameter to set the category to fetch:

static func events(forLast days: Int = 360, category: EOCategory) -> Observable<[EOEvent]> {

You also need to update calls to build the open and close observables using the endpoint provided by the category.

If you didn’t notice it before, a category object initializes with an endpoint string. You can use that string to fetch events in this category from the API. Replace the first two method lines with:

let openEvents = events(forLast: days, closed: false, endpoint: category.endpoint)
let closedEvents = events(forLast: days, closed: true, endpoint: category.endpoint)

With that last change, you’re done updating the service! Though you can‘t fully build the application yet, so move on to CategoriesViewController to add some interesting Rx action.

Incrementally updating the UI

Downloading events for each category revolves around using flatMap to produce as many event download observables as there are categories, then merge them. You’ve probably guessed where this is all going.

In CategoriesViewController.swift inside startDownload(), you should spot a line where Xcode complains about a missing parameter; replace the code that creates the downloadedEvents observable with the following:

let downloadedEvents = eoCategories
  .flatMap { categories in
    return Observable.from(categories.map { category in
      EONET.events(forLast: 360, category: category)
    })
  }
  .merge()

First, you get all the categories. You then call flatMap to transform them into an observable emitting one observable of events for each category. You then merge all these observables into a single stream of event arrays.

You need to replace the code that creates updatedCategories to make use of all the changes you’re doing. Replace the whole piece of code inside startDownload() that sets updatedCategories with:

let updatedCategories = eoCategories.flatMap { categories in
  downloadedEvents.scan(categories) { updated, events in
    return updated.map { category in
      let eventsForCategory = EONET.filteredEvents(events: events, forCategory: category)
      if !eventsForCategory.isEmpty {
        var cat = category
        cat.events = cat.events + eventsForCategory
        return cat
      }
      return category
    }
  }
}

Remember the scan(_:accumulator:) operator from the previous chapter? For every element emitted by its source observable, it calls your closure and emits the accumulated value. In your case, this accumulated value is the updated list of categories.

So every time a new group of events arrives, scan emits a category update. Since the updatedCategories observable is bound to the categories relay, the table view updates.

You have, in just a few lines of code, performed an elaborate sequence of API requests to produce timely updates.

But wait, there’s…

Just one more thing

Say you have 25 categories, which trigger two API requests each. That’s fifty API requests going out simultaneously to the EONET server. You want to limit the number of concurrent outgoing requests so you don’t hit the free-use threshold of the APIs.

There’s a simple but powerful change that completely turns your chain of operators into a threshold queue.

Replace the merge() call used when creating the downloadedEvents observable with:

.merge(maxConcurrent: 2)

This very simple change means that regardless of the number of event download observables flatMap(_:) pushes to its observable, only two will be subscribed to at the same time. Since each event download makes two outgoing requests (for open events and closed events), no more than four requests will fire at once. Others will be on hold until a slot is free.

Build and run the project and play around a bit — isn’t reactive UI simply the best?

Hopefully you’ve seen the depth and power of RxSwift! It takes your code to a new level of abstraction, where you rely on powerful tools to express complex tasks with clarity.

Challenges

Challenge 1

Start from the final project in this chapter. Place an activity indicator in the navigation bar and start its spinning animation when you start fetching the events and hide the spinner once you’ve finished fetching all data from the network.

To help you in this task, look up the do() operator in RxSwift which lets you perform side effects. This operator is very handy when you want to intercept events in an observable (subscription, values, completion, error or disposal) and execute some code without changing the sequence itself. For this challenge, do(onCompleted:) will be the variant you want to use.

Challenge 2

The first challenge was cool, but you can do even better. Add a download progress indicator showing during the events download. You’ll have to find the right spot to insert this in your code.

To solve this challenge, think about using the scan(_:accumulator:) operator you learned about in the previous chapeter. Also, the do operator will come handy in its do(onNext:) variant that lets you perform side effects every time an observable emits a new value.

You can complete this challenge in different ways so in the challenge folder for this chapter you will find two separate solutions. Did you come up with one of those on your own?

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.