Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

12. Beginning RxCocoa
Written by Shai Mishali

In previous chapters, you were introduced to the basics of RxSwift, its functional parts and how to create, subscribe and dispose observables. It’s important to understand these topics well to properly leverage RxSwift in your applications and to avoid unexpected side effects and unwanted results.

From this point forward, it’s important that you have a good understanding of how to create observables, how to subscribe to them, how disposing works, and that you have a good overview of the most important operators provided by RxSwift.

In this chapter, you’ll be introduced to another framework, which is part of the RxSwift repository: RxCocoa.

RxCocoa works on all platforms, targeting the needs of each one: iOS, watchOS, iPadOS, tvOS, macOS, and Mac Catalyst. Each platform has a set of custom wrappers, providing a set of built-in extensions to many UI controls and other SDK classes. In this chapter, you will use the ones provided for iOS on the iPhone and iPad.

Getting started

The starter project for this chapter is an iOS application named Wundercast. As suggested by the name, it’s a weather application using the current weather information provided by OpenWeatherMap http://openweathermap.org. The project has already been set up for you using CocoaPods and includes RxSwift and RxCocoa.

Before starting, open Podfile and check the project’s dependencies to better understand what you will be using in this chapter. To install RxCocoa, you have an extra line to include the relevant CocoaPod:

pod 'RxCocoa', '5.1.1'

RxCocoa is released alongside RxSwift. Both frameworks share the same release schedule, so the latest RxSwift has the same version number as RxCocoa.

Now, open Terminal and navigate to the root of the project. Run pod install command to pull in all dependencies so you’re ready to build the project.

Your workspace is now ready with both RxSwift and RxCocoa installed. I recommend that you open the workspace, navigate the pod project, and inspect what comes with RxCocoa. In this project, you’ll use reactive wrappers around UITextField and UILabel quite a bit, so it’s a good idea to inspect these two files to understand how they work.

Open UITextField+Rx.swift and check its contents. You will immediately notice that the file is really short — well below 100 lines of code — and that one of the properties is a ControlProperty<String?> named text.

What’s a ControlProperty, you ask? Don’t worry — you’ll learn about this a bit later. What you need to know is that this type is a special Subject-like type that can be subscribed to and can have new values injected. The name of the property gives you a good idea about what can be observed: text means that the property is directly related to the text inside the UITextField.

Now open UILabel+Rx.swift. Here you can see two new properties: text and attributedText. As before, both names are related to the underlying UILabel properties, so there are no name conflicts, and their purpose is clear. There’s a new type used in both called Binder.

Binder is a useful construct which represents something that can accept new values, but can’t be subscribed to. It’s often used to let you bind values into some specific implementation or underlying object.

Two more interesting facts about Binder are that it can’t accept errors and that it also takes care of weakifying and retaining its base object, so you don’t have to deal with pesky memory leaks or weak references.

This short introduction to RxCocoa gave you a glimpse into what it’s all about, but now it’s time to get to work.

Configuring the API key

OpenWeatherMap requires an API key to work, so sign up by following the instructions at https://home.openweathermap.org/users/sign_up.

Once you’ve signed up, navigate to the API key dedicated page https://home.openweathermap.org/api_keys and generate a new key to use in this project.

Copy the API key and paste it in ApiController.swift at the following spot:

private let apiKey = "Your Key"

At this point, you’re ready to proceed and receive data from the API.

Using RxCocoa with basic UIKit controls

First, make sure you’ve completed the setup by building the project; you’re now ready to input some data and ask the API to return the weather of a given city along with the temperature, humidity, and the city name. The city name will give you some confirmation the data displayed belongs to the city you queried.

Displaying the data using RxCocoa

If you run the project, you’ll notice there are placeholders for all label elements on the screen. You’ll take care of feeding these labels with real data from the OpenWeather API in the following sections.

In ApiController.swift, you’ll see a Decodable-conforming struct which will be used as a data model to correctly map the JSON response to something more easily digested by Swift:

struct Weather: Decodable {
  let cityName: String
  let temperature: Int
  let humidity: Int
  let icon: String
  ...
}

Still in ApiController.swift, take a look at the following method:

func currentWeather(for city: String) -> Observable<Weather> {
  // Placeholder call
  return Observable.just(
    Weather(
      cityName: city,
      temperature: 20,
      humidity: 90,
      icon: iconNameToChar(icon: "01d"))
  )
}

This method returns a fake city (you’ll use RxSwift) and displays some dummy data, which you can use instead of real data until you retrieve real weather information from the server.

Having dummy data helps simplify the development process and gives you the chance to work with an actual data structure, even without a working internet connection.

Open ViewController.swift; this is the one single view controller present in this project. The main goal of this project is to connect this single view controller to ApiController, which is going to provide the data.

This will result in a unidirectional data flow:

As explained in previous chapters, observables are entities capable of notifying subscribers that some data has arrived or changed, pushing values to be processed.

For this reason, the correct place to subscribe to an observable while working in view controllers is inside viewDidLoad. This is because you need to subscribe as early as possible, but only after the view has been loaded.

Subscribing in a different lifecycle event might lead to missed events, duplicate subscriptions, or parts of the UI that might be visible before you bind data to them.

Therefore, you have to create all subscriptions before the application creates or requests data that needs to be processed and displayed to the user.

To retrieve the data, add the following code to the end of viewDidLoad:

ApiController.shared.currentWeather(for: "RxSwift")
  .observeOn(MainScheduler.instance)
  .subscribe(onNext: { data in
    self.tempLabel.text = "\(data.temperature)° C"
    self.iconLabel.text = data.icon
    self.humidityLabel.text = "\(data.humidity)%"
    self.cityNameLabel.text = data.cityName
  })

Build and run your app, and you should have the following result:

The application is correctly displaying the dummy data, but there are two problems:

  1. There’s a compiler warning.
  2. You still don’t make use of the input text field.

The first problem is pointed out by the following warning displayed by Xcode:

As in previous chapters, a subscription returns a Disposable which lets you cancel the subscription when necessary. In this case, the subscription should be canceled when the view controller is dismissed to avoid potential memory leaks. To achieve this, add the following property to the view controller class:

private let bag = DisposeBag()

Then, transform the previous code, adding the correct disposed(by:) method at the end of your subscription chain:

ApiController.shared.currentWeather(for: "RxSwift")
  .observeOn(MainScheduler.instance)
  .subscribe(onNext: { data in
    self.tempLabel.text = "\(data.temperature)° C"
    self.iconLabel.text = data.icon
    self.humidityLabel.text = "\(data.humidity)%"
    self.cityNameLabel.text = data.cityName
  })
  .disposed(by: bag)

This will tie the lifecycle of your subscription to that of your DisposeBag, and with that - of your view controller. This guards against wasting resources, but also avoids unexpected events or other side effects that can happen when a subscription isn’t disposed.

You’ve solved the first issue, so you can turn your attention to the text field. As previously mentioned, RxCocoa adds a lot on top of Cocoa, so you can start using this functionality to achieve your ultimate goal. The framework uses the power of protocol extensions and adds the rx namespace to many of UIKit’s components. Type searchCityName.rx. to see the available properties and methods:

There’s one you’ve already explored before: text. This property returns an observable that is a ControlProperty<String?>, which conforms to both ObservableType and ObserverType so you can subscribe to it and also add new values onto it, thus setting the field text.

Knowing the basics behind ControlProperty, you can improve the code to take advantage of the text field to display the city name in the dummy data. Add to viewDidLoad():

searchCityName.rx.text.orEmpty
  .filter { !$0.isEmpty }
  .flatMap { text in
    ApiController.shared
      .currentWeather(for: text)
      .catchErrorJustReturn(.empty)
  }

The above code will return a new observable with the data to display. You add the .orEmpty property after .text, which simply emits an empty string if nil is emitted, being the equivalent of .map { $0 ?? "" }. Since currentWeather does not accept empty values, you filter those out. Then, you fetch the weather data by using the provided ApiController class.

You’ve already completed similar tasks involving networking in the previous chapters so you won’t go into more detail about that here.

Continue your previous block of code by switching to the correct thread and displaying the data:

  .observeOn(MainScheduler.instance)
  .subscribe(onNext: { data in
    self.tempLabel.text = "\(data.temperature)° C"
    self.iconLabel.text = data.icon
    self.humidityLabel.text = "\(data.humidity)%"
    self.cityNameLabel.text = data.cityName
  })
  .disposed(by: bag)

Once you have switched to the MainScheduler and the main thread, you update all UI controls with the current weather data.

Note: The concept of Schedulers is beyond the scope of this chapter, but you’ll learn much more about it in Chapter 15, “Intro to Schedulers.”

The diagram below should help you visualize the flow of the code:

At this point, whenever you change the input, the label will update with the name of the city — but right now it always returns your dummy data. You know the application displays the dummy data correctly, so it’s time to get the real data from the API.

Note: The catchErrorJustReturn operator will be explained later in this book. It’s required to prevent the observable from being disposed when you receive an error from the API. For instance, an invalid city name returns 404 as an error for NSURLSession. In this case, you want to return an empty response so the app won’t stop working if it encounters an error.

Retrieving data from the OpenWeather API

To retrieve live weather data from the API, you’ll need an active internet connection. The API returns a structured JSON response. The following are the useful bits:

{
  "weather": [
    {
      "id": 741,
      "main": "Fog",
      "description": "fog",
      "icon": "50d"
    }
  ],
}

The above data is related to the current weather; the icon elements is used to display the correct icon for the current conditions. The section below deals with the temperature and humidity data:

"main": {
  "temp": 271.55,
  "pressure": 1043,
  "humidity": 96,
  "temp_min": 268.15,
  "temp_max": 273.15
}

Don’t freak out — those temperatures are in Kelvin, not Celsius or Fahrenheit! :]

Inside ApiController.swift, there’s a method named iconNameToChar that takes a String (more precisely, the icon data from the above JSON) and returns another String, which is the UTF-8 code of the weather icon that visually represents the current weather in your application. In the same file, there’s a convenience method buildRequest to create network requests; this uses RxCocoa’s wrapper for URLSession to perform network requests. This method is responsible for:

  • Getting the base URL and appending the components to correctly build the GET (or POST) request.

  • Using the API key you generated at the beginning of this chapter.

  • Setting the content type of the request to application/json.

  • Asking for metrics as units (in this case, degrees Kelvin).

  • Returning the data Observable, which will later be decoded using a JSONDecoder in the currentWeather method.

The last part is collapsed in a single return line:

return session.rx.data(request: request)

This uses the data method from the rx extension for URLSession. You’ll decode this Data later on using a JSONDecoder.

Switching from the dummy data to the actual data request is relatively straightforward. You need to replace the Observable.just([...]) call with a real data network request. The OpenWeatherMap API documentation http://openweathermap.org/current explains how to request the current weather for a given city name via api.openweathermap.org/data/2.5/weather?q={city name}.

In ApiController.swift, replace the dummy currentWeather(for:) method with:

func currentWeather(for city: String) -> Observable<Weather> {
  buildRequest(pathComponent: "weather", params: [("q", city)])
    .map { data in
      try JSONDecoder().decode(Weather.self, from: data)
    }
}

The request returns a Data object, which can be decoded to a Weather struct, thanks to its conformance to Decodable.

It’s always good to have a visualization when working with Rx in general, and an updated diagram with a bit more detail will probably help you understand what’s happening inside ApiController:

Build and run, and enter London for the city. You should receive the following result:

Your app now correctly displays the data retrieved from the server. You’ve used a couple of RxCocoa features so far but you’re going to see the real benefits when you move on to RxCocoa’s more advanced features in the next section.

Note: If you’re interested, try a little experiment. Remove the catchErrorJustReturn operator inside flatMap. As soon as you receive a 404 due to an invalid city name, which you’ll see in the logs, the application will stop working correctly because your observable has errored out and is then disposed.

Binding observables

Binding is somewhat controversial: for example, Apple never released their binding system, named Cocoa Bindings, on iOS, even though it had been an important part of macOS for a long time. Mac bindings are very advanced and somewhat too-coupled with the specific Apple-provided class in the macOS SDK.

RxCocoa offers a somewhat simpler solution, which depends only on a few types included with the framework. Since you’re already feeling comfortable with RxSwift code, you’re bound (pun intended) to figure bindings out very quickly.

An important thing to know here is that in RxCocoa, a binding is a uni-directional stream of data. This greatly simplifies data flow in the app, so you won’t cover bidirectional bindings in this book.

What are binding observables?

The easiest way to understand binding is to think of the relationship as a connection between two entities:

  • A producer, which produces the value.
  • A consumer, which processes the values from the producer.

A consumer cannot return a value. This is a general rule when using bindings in RxSwift.

Note: If you want to experiment later with bidirectional bindings (for example, between a data model property and a text field), this could be modeled by using four of these entities: two producers, and two consumers. This, as you can imagine, increases the code complexity considerably — still, if you’re in the mood to play around, it can be fun.

The fundamental method for binding is bind(to:), used to bind an observable to another entity. It’s required that the consumer conforms to ObserverType, a write-only entity that can only accept new events but cannot be subscribed to.

The only type we have bundled with RxSwift which is an ObserverType is Subject, which you previously learned about and lets you not only write events to but also subscribe to it since it conforms to both ObserverType and ObservableType.

Subjects are extremely important when working with the imperative nature of Cocoa, considering that the fundamental components like UILabel, UITexField, UIImageView, etc… have mutable data that can be set or get.

Note: Aside from ObserverType-conforming objects, you can also use bind(to:) on Relays. Those bind(to:) methods are separate overloads since Relays don’t conform to ObserverType.

Finally, an interesting fact is that bind(to:) is an alias, or syntactic sugar, for subscribe(). Calling bind(to: observer) will internally call subscribe(observer). The former is simply in place to create a more meaningful and intuitive syntax.

Using binding observables to display data

Now that you know what bindings are, you can start to integrate them into your app. In the process, you’ll make the whole code a little more elegant and turn the search result into a reusable data source.

The first change to apply is to refactor the long observable that assigns the data to the correct UILabel with subscribe(onNext:). Open ViewController.swift and in viewDidLoad() replace the complete subscription code to searchCityName with:

let search = searchCityName.rx.text.orEmpty
  .filter { !$0.isEmpty }
  .flatMapLatest { text in
    ApiController.shared
      .currentWeather(for: text)
      .catchErrorJustReturn(.empty)
  }
  .share(replay: 1)
  .observeOn(MainScheduler.instance)

There are two changes between this code and the one before it.

The first one is changing flatMap to flatMapLatest, which will cancel any previous network requests when a new one starts. Without it, you might get multiple results as you type a city name since nothing takes care of canceling previously-pending requests.

The second one is adding share(replay: 1) to the subscription, which makes your stream reusable and transforms a single-use data source into a multi-use Observable.

The power of the latter change will be covered later in the chapter dedicated to MVVM, but for now, simply realize that observables can be heavily reusable entities in Rx, and that correct modeling can make a long, difficult-to-read, single-use observer into a multi-use and easy to understand observer instead.

With this small change, it’s possible to process every single parameter from a different subscription, mapping the value required to be displayed. For example, here’s how to get the temperature as a string out of the shared data source observable:

search.map { "\($0.temperature)° C" }

This will create an observable that returns the required string to be displayed as temperature.

To try creating your first binding, use bind(to:) to connect the original data source to the temperature label. Add to viewDidLoad():

search.map { "\($0.temperature)° C" }
  .bind(to: tempLabel.rx.text)
  .disposed(by: bag)

Build and run to display the temperature using this new and shiny RxCocoa-powered binding:

Now the application only displays the temperature, but you can restore the previous functionality by simply applying the same pattern to the rest of the labels.

search.map(\.icon)
  .bind(to: iconLabel.rx.text)
  .disposed(by: bag)

search.map { "\($0.humidity)%" }
  .bind(to: humidityLabel.rx.text)
  .disposed(by: bag)

search.map(\.cityName)
  .bind(to: cityNameLabel.rx.text)
  .disposed(by: bag)

Now the application displays the data you request from the server, using a single observable source named search, and binds different pieces of the data to each label on the screen.

Improving the code with Traits

RxCocoa offers even more advanced features to make working with Cocoa and UIKit a breeze. Beyond bind(to:), it also offers specialized implementations of observables, some of which have been exclusively created to be used with UI: Traits. Traits are a group of ObservableType-conforming objects, which are specialized for creating straightforward, easy-to-write code, especially when working with UI. Let’s have a look!

Note: Very much like the RxSwift traits you learned about in section one of this book, the RxCocoa traits are specializations that are helpful to use, but optional, if you prefer to stick to the observables you already know so well.

What are ControlProperty and Driver?

Traits are described as the following in the official documentation:

Traits… help communicate and ensure observable sequence properties across interface boundaries.

It might be confusing at first, but the rules of using RxCocoa’s traits make the whole concept a little easier to understand. The rules of these are:

  • They can’t error out.
  • They are observed and subscribed on the main scheduler.
  • They share resources, which is not surprising since they are both derived from an entity called SharedSequence. Driver automatically gets share(replay: 1), while Signal gets share().

These entities ensure something is always displayed in the user interface and that they are always able to be handled by the user interface.

The RxCocoa Traits are:

  • ControlProperty and ControlEvent
  • Driver
  • Signal

ControlProperty is not new; you used it just a little while ago to bind the data to the correct user interface component using the dedicated rx namespace. As its name suggests, it is used to represent properties of objects that can both be read and modified.

ControlEvent is used to listen for a certain event of the UI component, like the press of the “Return” button on the keyboard while editing a text field. A control event is available if the component uses UIControl.Events to keep track of its current status.

Driver is a special observable with the same constraints as explained before, so it can’t error out. All processes are ensured to execute on the main thread, which avoids making UI changes on background threads, and it always shares resources and replays its latest value to new consumers upon subscription.

Signal is identical to Driver in the sense that it also delivers events on the main scheduler, doesn’t error out, and shares its resources, but it doesn’t replay its latest value to new consumers upon subscription.

You can think of Signal as useful for modeling events, where a Driver is more suitable for modeling state, due to their different replay strategies.

Traits in general are an optional part of the framework; you’re not forced to use them. Feel free to stick to observables and subjects, while making sure you are creating the right task in the right scheduler. But if you want some compile-time guarantees, and predictable rules when dealing with your UI, these components can be extremely powerful and save you time. It’s easy to forget to call .observeOn(MainScheduler.instance) and end up creating UI processes on a background thread.

Don’t worry if Driver and ControlProperty seem confusing right now. Like a lot of things in Rx, they will make more sense once you dive into the code.

Improving the project with Driver and ControlProperty

After some theory, it’s time to apply all those nice concepts to your application, make sure all the tasks are performed in the right thread, and ensure that nothing will error out and stop subscriptions from delivering results.

The first step is to transform the weather data observable into a Driver. Find where you define the search constant in viewDidLoad(), and replace the code with:

let search = searchCityName.rx.text.orEmpty
  .filter { !$0.isEmpty }
  .flatMapLatest { text in
    ApiController.shared
      .currentWeather(for: text)
      .catchErrorJustReturn(.empty)
  }
  .asDriver(onErrorJustReturn: .empty)

The key line of code here is the one at the bottom: .asDriver(...). This is the method that converts your observable into a Driver. the onErrorJustReturn parameter specifies a default value to be used in case the converted observable errors out — thus eliminating the possibility for the driver itself to emit an error.

You might have also noticed that auto-completion offers some other variants to asDriver(onErrorJustReturn:):

  • asDriver(onErrorDriveWith:): With this method, you can handle the error manually and return a new Driver generated for this purpose only.

  • asDriver(onErrorRecover:): Can be used alongside another existing Driver. This will come in play to recover the current Driver that just encountered an error.

But wait! The application doesn’t build any more because bind(to:) doesn’t exist for Driver. What to do?

Driver comes with a different method, aptly named — drive. Simply can replace all the bind(to:) calls with drive and you’ll be good to go.

search.map { "\($0.temperature)° C" }
  .drive(tempLabel.rx.text)
  .disposed(by: bag)

search.map(\.icon)
  .drive(iconLabel.rx.text)
  .disposed(by: bag)

search.map { "\($0.humidity)%" }
  .drive(humidityLabel.rx.text)
  .disposed(by: bag)
search.map(\.cityName)
  .drive(cityNameLabel.rx.text)
  .disposed(by: bag)

This will restore the correct UI behavior of the application while taking advantage of the power of Driver. drive works quite similarly to bind(to:); the difference in the name better expresses the intent while using RxCocoa’s Traits.

At this point, the application takes advantage of a lot of the shiny parts of RxCocoa, but there’s still something you can improve. The application uses way too many resources and makes too many API requests because it fires a request every time you type a character. A bit of an overkill, don’t you think?

Find this line:

let search = searchCityName.rx.text.orEmpty

And replace it with:

let search = searchCityName.rx
  .controlEvent(.editingDidEndOnExit)
  .map { self.searchCityName.text ?? "" }
  // rest of your .filter { }.flatMapLatest { } continues here

Now the application retrieves the weather only when the user hits the “Search” button on the keyboard. You’re not making unnecessary network requests, and the code is controlled at compile time by Traits.

The original schema used a single observable that updated the entire UI; through a breakdown of multiple blocks, you’ve switched from subscribe to bind(to:) and then to drive and reused the same observables across the view controller. This approach makes the code quite reusable and easy to work with.

For example, if you wanted to add the current barometric pressure to the user interface, all you would have to do is to add the property to the structure, then add another UILabel and map that property to the new label. Nice!

Recap of Traits in RxSwift and RxCocoa

You’re probably overwhelmed by the number of traits and entities that are part of RxCocoa and RxSwift, so if you need a recap, here’s a table that sums up all of them:

Disposing with RxCocoa

The last topic of this chapter goes beyond the project and is pure theory. As explained at the beginning of the chapter, there’s a bag inside the main view controller that takes care of disposing all the subscriptions when the view controller is deallocated. But in this example, there’s no usage of weak or unowned in all closures. Why?

The answer is simple: This application is a single view controller and the main view controller is always on the screen while the application is running — so there’s no need to guard against retaining cycles or wasted memory.

Unowned vs. weak with RxCocoa

The rules for using weak and unowned are the same you would follow when using regular Swift closures, and are mainly relevant when calling the closure-variations of Rx, such as subscribe(onNext:). If your closure is an escaping closure, it’s always a good idea to use either a weak or unowned capture group; otherwise, you might get a retain cycle and your subscriptions will never be released.

Using weak means you’ll get an Optional reference to self, and using unowned will provide an implicitly unwrapped reference to self. you can think of weak providing Self? and unowned providing Self!, meaning that you have to be extra careful when using unowned, as it’s practically a force-unwrap; if the object isn’t there, your app will crash.

For these reasons, the raywenderlich.com Swift Guidelines https://bit.ly/3gA2m4N recommends against using unowned at all.

Challenge

Challenge: Switch from Celsius to Fahrenheit

Your challenge in this chapter is to add a switch to change from Celsius to Fahrenheit. This task can be achieved in different ways:

  • Change the units request query parameter from metric to imperial.
  • Map the Celsius value with the mathematical conversion: temperature * 1.8 + 32.

Technically, each solution has its own obstacles to overcome. The first approach requires a change in ApiController.swift with an addition of a Subject to process the change right away and request the new data.

The second approach is shorter and probably easier. You can achieve this by combining the search observable with the control property of UISwitch. This solution is the recommended one for this chapter, especially when you consider that more advanced usages and architectures will be explained later in this book.

Generally, try to be as pragmatic as possible and don’t over-engineer this solution. In the next chapter, you will see more advanced usages of RxCocoa, so take some time to play with the basics of this framework first.

Where to go from here?

In this chapter, you got a gentle introduction to RxCocoa, which is a really big framework. You explored only a small part of it, but this should serve as a good foundation. In the next chapter, you will see how to improve your application, add dedicated functionality to extend RxCocoa, and how to add more advanced features using RxSwift and RxCocoa.

Before proceeding, take some time to play around with RxCocoa and its .rx-namespaced properties. Considering this framework has dozens of extensions available, it’s a good idea to look at a couple of examples first.

UIButton

You’ll often have a button in your View Controller, and being able to get a stream of taps from that button is extremely useful. You can get this stream by simply using button.rx.tap, which is a ControlEvent<Void>.

It also features an isEnabled Binder of type Bool, which is applied to all UIControls, so if you have an Observable<Bool> which defines if a control should be enabled, you can simply bind it directly to button.rx.isEnabled.

UIActivityIndicatorView

UIActivityIndicatorView is one of the most used UIKit components. This extension has the following property available:

public var isAnimating: Binder<Bool>

Again, the name is self-explanatory and is related to the original isAnimating property.

Just like with UILabel, the property is of type Binder and the result is that it can be bound to an observable to notify a background activity. You might remember using this in the challenges of Chapter 10, “Combining Operators in Practice.”

UIProgressView

UIProgressView is a less common component, but it’s also covered in RxCocoa and has the following property:

public var progress: Binder<Float>

As for all the other similar components, the UIProgressBar can be bound to an observable. For example, assume an uploadFile(with:) function is producing an observable of a task uploading a file to a server, providing intermediate events with bytes sent and total bytes. This code could look much like this:

let progressBar = UIProgressBar()
let uploadFile = uploadFile(with: fileData)
uploadFile
  .map { sent, totalToSend in
    sent / totalToSend
  }
  .bind(to: progressBar.rx.progress)
  .disposed(by: bag)

The result is that the progress bar is updated every single time an intermediate value is provided, and the user has some visual indication of the upload’s progress.

At this point, it’s your turn. The more time you spend playing with these extensions, the more you will be comfortable using them in the next chapter — and in future applications.

Note: RxCocoa is a constantly improving framework. If you think any controls or extensions are missing, you can create them and submit a pull request to the official repository. Contributions are welcomed (and encouraged!) by the ever-growing community.

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.