Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

12. Beginning RxCocoa
Written by Shai Mishali

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

private let apiKey = "Your Key"

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.

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

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
  })

private let bag = DisposeBag()
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)

searchCityName.rx.text.orEmpty
  .filter { !$0.isEmpty }
  .flatMap { text in
    ApiController.shared
      .currentWeather(for: text)
      .catchErrorJustReturn(.empty)
  }
  .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)

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"
    }
  ],
}
"main": {
  "temp": 271.55,
  "pressure": 1043,
  "humidity": 96,
  "temp_min": 268.15,
  "temp_max": 273.15
}
return session.rx.data(request: request)
func currentWeather(for city: String) -> Observable<Weather> {
  buildRequest(pathComponent: "weather", params: [("q", city)])
    .map { data in
      try JSONDecoder().decode(Weather.self, from: data)
    }
}

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.

What are binding observables?

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

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.

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)

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

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)

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!

What are ControlProperty and Driver?

Traits are described as the following in the official documentation:

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.

let search = searchCityName.rx.text.orEmpty
  .filter { !$0.isEmpty }
  .flatMapLatest { text in
    ApiController.shared
      .currentWeather(for: text)
      .catchErrorJustReturn(.empty)
  }
  .asDriver(onErrorJustReturn: .empty)
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)
let search = searchCityName.rx.text.orEmpty
let search = searchCityName.rx
  .controlEvent(.editingDidEndOnExit)
  .map { self.searchCityName.text ?? "" }
  // rest of your .filter { }.flatMapLatest { } continues here

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?

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.

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:

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.

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>.

UIActivityIndicatorView

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

public var isAnimating: Binder<Bool>

UIProgressView

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

public var progress: Binder<Float>
let progressBar = UIProgressBar()
let uploadFile = uploadFile(with: fileData)
uploadFile
  .map { sent, totalToSend in
    sent / totalToSend
  }
  .bind(to: progressBar.rx.progress)
  .disposed(by: bag)
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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now