Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

17. Creating Custom Reactive Extensions
Written by Florent Pillet

After being introduced to RxSwift, RxCocoa, and learning how to create tests, you have only scratched the surface with how to create extensions using RxSwift on top of frameworks created by Apple or by third parties. Wrapping an Apple or third party framework’s component was introduced in the chapter about RxCocoa, so you’ll extend your learning as you work your way through this chapter’s project.

In this chapter, you will create an extension to URLSession to manage the communication with an endpoint, as well as managing the cache and other things which are commonly part of a regular application. This example is pedagogical; if you want to use RxSwift with networking, there are several libraries available to do this for you, including RxAlamofire, which we also cover later in this book.

Getting started

To start, you’ll need a API key for Giphy https://giphy.com, one of the most popular GIF services on the web. To get the API key, navigate to the official docs https://developers.giphy.com/docs/.

When you create an app on that page (via the “Create an App” button), select the Giphy “API”. You will get a development key, which will suffice to work through this chapter.

The API key is displayed under the name of your newly created app like so:

Start by opening Terminal. Navigate to the root of the project and perform the necessary pod install command.

Open ApiController.swift and copy the key into the correct place:

private let apiKey = "Your Key"

Once you’ve completed this step, you will have all the necessary dependencies installed so you can build and run the application.

How to create extensions

Creating an extension over a Cocoa class or framework might seem like a non-trivial task; you will see that the process can be tricky and your solution might require some up-front thinking before continuing.

The goal here is to extend URLSession with the rx namespace, isolating the RxSwift extension, and making sure collisions are nearly impossible if you (or your team) need to extend this class further.

How to extend URLSession with .rx

To enable the .rx extension for URLSession, open URLSession+Rx.swift and add the following:

extension Reactive where Base: URLSession {

}

The Reactive extension, through a very clever protocol extension, exposes the .rx namespace over URLSession. This is the first step in extending URLSession with RxSwift. Now it’s time to create the real wrapper.

How to create wrapper methods

You’ve exposed the .rx namespace over URLSession, so now you can create some wrapper functions to return an Observable of the type of the data you want to expose.

APIs can return various types of data, so it’s a good idea to have some checks on the type of data your app expects. You want to create the wrappers for handling the following types of data:

  • Data: just plain data.
  • String: data as text.
  • JSON: an instance of a JSON object.
  • Decodable: decoding into a Decodable-conforming object.
  • Image: an instance of image.

These wrappers are going to ensure you can get the exact type you need. Otherwise, an error will be sent and the application will error out without crashing.

This wrapper, and one that will be used to create all the others, is the one that returns the HTTPURLResponse and the resulting Data. Your goal is to have an Observable<Data>, which will be used to create the remaining three operators:

Start by creating the skeleton of the main response function, so you know what to return. Add inside the extension you just created:

func response(request: URLRequest) -> Observable<(HTTPURLResponse, Data)> {
  return Observable.create { observer in
    // content goes here
    return Disposables.create()
  }
}

You might be already making some guesses on what this method is going to do. The HTTPURLResponse is the part you will check to ensure the request has been successfully processed, while Data is the actual data returned by it.

URLSession is based on callbacks and tasks. For example, the built-in method that sends a request and receives back the server response is dataTask(with:completionHandler:). This method uses a callback to manage the result, so the logic of your observable has to be managed inside the required closure.

To do that, add the following inside Observable.create:

let task = self.base.dataTask(with: request) { data, response, error in

}
task.resume()

After creation you need to resume (or start) the task as it does not run right away, so the resume() method will trigger the request. The callback will later handle the result from the request.

Note: The use of the resume() method is what is known as imperative programming. You’ll see exactly what this means later on.

Now that the task is in place, there’s a change to perform before proceeding. In the previous block, you were returning a Disposable.create(), which would simply do nothing if the Observable was disposed. It’s better to cancel the request so that you don’t waste any resources.

To do this, replace return Disposables.create() with:

return Disposables.create { task.cancel() }

Now that you have the Observable with the correct lifetime strategy, it’s time to validate the data you receive before sending any event to this instance.

To achieve this, add the following code inside your task’s closure:

guard let response = response,
      let data = data else {
  observer.onError(error ?? RxURLSessionError.unknown)
  return
}

guard let httpResponse = response as? HTTPURLResponse else {
  observer.onError(RxURLSessionError.invalidResponse(response: response))
  return
}

Both guard statements confirm the request has been successfully performed before notifying all the subscriptions. After ensuring the request has been correctly completed, this observable needs some data.

Add the following code immediately after the code you just added before:

observer.onNext((httpResponse, data))
observer.onCompleted()

This sends the event to all subscribers, then immediately completes the Observable. It wouldn’t make sense to keep the observable alive and perform other requests, which is more appropriate for things such as socket communication.

This is the most basic operator to wrap URLSession. You’ll need to wrap a few more things to make sure the application is dealing with the correct kind of data. The good news is that you can reuse this method to build the rest of the convenience methods.

Start by adding the one returning a Data instance:

func data(request: URLRequest) -> Observable<Data> {
  return response(request: request).map { response, data -> Data in
    guard 200 ..< 300 ~= response.statusCode else {
      throw RxURLSessionError.requestFailed(response: response, data: data)
    }

    return data
  }
}

The Data observable is the root of all the others. Data can be converted to a String, JSON object or UIImage. Add the following to return a String:

func string(request: URLRequest) -> Observable<String> {
  return data(request: request).map { data in
    return String(data: data, encoding: .utf8) ?? ""
  }
}

A JSON data structure is a simple structure to work with, so a dedicated conversion is more than welcome. Add:

func json(request: URLRequest) -> Observable<Any> {
  return data(request: request).map { data in
    return try JSONSerialization.jsonObject(with: data)
  }
}

As long as you’re dealing with JSON, you can also add a dedicated method to decode a Decodable object. Add the following method:

func decodable<D: Decodable>(request: URLRequest,
                             type: D.Type) -> Observable<D> {
  return data(request: request).map { data in
    let decoder = JSONDecoder()
    return try decoder.decode(type, from: data)
  }
}

Finally, implement the last one to return an instance of UIImage:

func image(request: URLRequest) -> Observable<UIImage> {
  return data(request: request).map { data in
    return UIImage(data: data) ?? UIImage()
  }
}

When you modularize an extension like you just did, you allow for better composability. For example, the last observable can be visualized in the following way:

Some of RxSwift’s operators, such as map, can be smartly assembled to avoid processing overhead so a multiple chain of maps will be optimized into a single call. Don’t worry about chaining them or including too much in the closures.

How to create custom operators

In the chapter about RxCocoa, you created a method to cache data. This looks like a good approach here, considering the size of some GIFs. Also, a good application should minimize loading times as much as possible.

A good approach in this case is to create a special operator to cache data that is only available for observables of type (HTTPURLResponse, Data). The goal is to cache as much as possible, so it sounds reasonable to create this operator only for observables of type (HTTPURLResponse, Data) and use the response object to retrieve the absolute URL of the request and use it as a key in the dictionary.

The caching strategy will be a simple Dictionary; you can later extend this basic behavior to persist the cache and reload it when reopening the app, but this goes beyond the current project‘s scope.

Create the cache dictionary at the top, before the RxURLSessionError’s definition:

private var internalCache = [String: Data]()

Then, create the extension which will target only observables of Data type:

extension ObservableType where Element == (HTTPURLResponse, Data) {

}

Inside this extension, you can create the cache() operator as shown:

func cache() -> Observable<Element> {
  return self.do(onNext: { response, data in
    guard let url = response.url?.absoluteString,
          200 ..< 300 ~= response.statusCode else { return }

    internalCache[url] = data
  })
}

To use the cache, make sure to modify data(request:)’s return statement to cache the response before returning its own result. You can simply insert only the .cache() part:

return response(request: request).cache().map { response, data -> Data in
  //...
}

To check if the data is already available, instead of firing a network request every time, add the following to the top of data(request:), before the return:

if let url = request.url?.absoluteString,
   let data = internalCache[url] {
  return Observable.just(data)
}

You now have a very basic caching system that extends only a certain type of Observable:

You can reuse the same procedure to cache other kinds of data, considering this is an extremely generic solution.

Using custom wrappers

You’ve created some wrappers around URLSession, as well as some custom operators targeting only some specific type of observables. Now it’s time to fetch some results and display some funny cat GIFs.

The current project already has the batteries included, so the only thing you need to provide is a list of GiphyGif structures coming from the Giphy API.

Open ApiController.swift and have a look at the search() method. The code inside prepares a proper request to the Giphy API, but at the very bottom it doesn’t make a network call. Instead it simply returns an empty observable (since this is placeholder code).

Now that you’ve completed your URLSession reactive extension, you can make use of it to get data from the network in the bespoke method and decode it to the proper model. Modify the return statement like so:

return URLSession.shared.rx
  .decodable(request: request, type: GiphySearchResponse.self)
  .map(\.data)

This will handle the request for a given query string, but the data is still not displayed. There’s one last step to be performed before the GIF actually pops up on screen.

Add the following to GifTableViewCell.swift, right at the end of downloadAndDisplay(gif stringUrl:):

let s = URLSession.shared.rx.data(request: request)
  .observeOn(MainScheduler.instance)
  .subscribe(onNext: { [weak self] imageData in
    guard let self = self else { return }

    self.gifImageView.animate(withGIFData: imageData)
    self.activityIndicator.stopAnimating()
  })
disposable.setDisposable(s)

The usage of SingleAssignmentDisposable() is mandatory to keep things performing well. When a download of a GIF starts, you should make sure it’s been stopped if the user scrolls away and doesn’t wait for the rendering of the image. To correctly balance this, prepareForReuse() has the following two lines already included in the starter code:

disposable.dispose()
disposable = SingleAssignmentDisposable()

The SingleAssignmentDisposable() will ensure only one subscription is ever alive at a given time for every single cell so you won’t bleed resources.

Build and run, type something in the search bar and you’ll see the app come to life.

Testing custom wrappers

Although everything seems to be working properly, it’s a good habit to create some tests and ensure everything keeps working correctly, especially when wrapping third party frameworks, or decoding responses to custom models.

Test suites ensure your implementation stays in good shape, and will help you find where the code is failing due to a breaking change or a bug.

How to write tests for custom wrappers

You were introduced to testing in the previous chapter; in this chapter, you’ll use a common library used to write tests on Swift called Nimble, along with its wrapper RxNimble.

RxNimble makes tests easier to write and helps your code be more concise. Instead of writing the classic:

let result = try! observabe.toBlocking().first()
expect(result).first != 0

You can write a shorter version:

expect(observable) != 0

Open the test file iGifTests.swift. Checking the import section, you can see the Nimble, RxNimble, OHHTTPStubs used to stub network requests and RxBlocking necessary to convert an asynchronous operation into a blocking ones.

At the end of the file, you can also find a short extension for BlockingObservable with a single function:

func firstOrNil() -> Element? {}

This would avoid abusing the try? method all through the test file. You’ll see this in use shortly.

At the top of the file, you’ll find a dummy JSON object to test with:

let obj = ["array": ["foo", "bar"], "foo": "bar"] as [String: AnyHashable]

Using this predefined data makes it easier to write tests for Data, String and JSON requests.

The first test to write is the one for the data request. Add the following test to the test case class to check that a request is not returning nil:

func testData() {
  let observable = URLSession.shared.rx.data(request: self.request)
  expect(observable.toBlocking().firstOrNil()).toNot(beNil())
}

As soon as you wrap up typing in the method, Xcode will display a diamond-shaped button in the editor gutter much like this (the line number might differ for you):

Click on the button and run the test. If the test succeeds, the button will turn green; if it fails, it will turn red. Hopefully you typed in all the code correctly, and you will see the button turn into a green checkmark.

Once the observable returning Data is tested and works correctly, the next one to test is the observable that handles String.

Considering that the original data is a JSON representation, and given that dictionary keys are unfortunately not guaranteed to be sorted, the result could be one of two:

{"array":["foo","bar"],"foo":"bar"}

Or:

{"foo":"bar","array":["foo","bar"]}

The test is then really straightforward to write. Add the following, taking in consideration that the JSON strings have to be escaped:

func testString() {
  let observable = URLSession.shared.rx.string(request: self.request)
  let result = observable.toBlocking().firstOrNil() ?? ""

  let option1 = "{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}"
  let option2 = "{\"foo\":\"bar\",\"array\":[\"foo\",\"bar\"]}"

  expect(result == option1 || result == option2).to(beTrue())
}

Press the test button for that new test, and once finished, move on to testing JSON parsing. The test requires a Dictionary to compare with.

Add the following code to cast the JSON response to a Dictionary and compare it to the original object.

func testJSON() {
  let observable = URLSession.shared.rx.json(request: self.request)
  let obj = self.obj
  let result = observable.toBlocking().firstOrNil()
  expect(result as? [String: AnyHashable]) == obj
}

The last test is to make sure that errors are returned properly. Comparing two errors is a rather uncommon procedure, so it doesn’t make sense to have an equal operator for an error. Therefore the test should use do, try and catch for the unknown error.

Add the following:

func testError() {
  var erroredCorrectly = false
  let observable = URLSession.shared.rx.json(request: self.errorRequest)
  do {
    _ = try observable.toBlocking().first()
    assertionFailure()
  } catch RxURLSessionError.unknown {
    erroredCorrectly = true
  } catch {
    assertionFailure()
  }
  expect(erroredCorrectly) == true
}

At this point your project is complete. You’ve created your own extensions on top of URLSession, and you also created some cool tests which will ensure your wrapper is behaving correctly. Testing wrappers like the one you’ve built is extremely important because Apple frameworks and other third party frameworks can feature breaking changes in major releases, so you should be prepared to act fast if a test breaks and the wrapper stops working.

Common available wrappers

The RxSwift community is very active, and there are a lot of extensions and wrappers already available. Some are based on Apple components, while some others are based on widely-used, third-party libraries found in many iOS and macOS projects.

You can find a list of up-to-date wrappers at http://community.rxswift.org.

Here’s a quick overview of the most common wrappers at present:

RxDataSources

RxDataSources is a UITableView and UICollectionView data source for RxSwift with some really nice features such as:

  • O(n) algorithm for calculating differences.
  • Heuristics to send the minimal number of commands to the sectioned view.
  • Support for extending already implemented views.
  • Support for hierarchical animations.

These are all important features, but my favorite is the O(n) algorithm to differentiate between two data sources: it ensures the application isn’t performing unnecessary calculations when managing table views.

Consider the code you write with the built-in RxCocoa table binding:

let data = Observable<[String]>.just(
  ["1st place", "2nd place", "3rd place"]
)

data.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { index, model, cell in
  cell.placeLabel.text = model
}
.disposed(by: bag)

This works perfectly with simple data sets, but lacks animations, support for multiple sections, and doesn’t extend very well. With RxDataSource correctly configured, the code becomes more robust:

// Configure sectioned data source
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, String>>()
Observable.just([SectionModel(model: "Position", items: ["1st", "2nd", "3rd"])])
  .bind(to: tableView.rx.items(dataSource: dataSource))
  .disposed(by: bag)

And the minimal configuration of the data source that needs to be done in advance looks like so:

dataSource.configureCell = { dataSource, tableView, indexPath, item in
  let cell = tableView.dequeueReusableCell(
    withIdentifier: "Cell", for: indexPath)
  cell.placeLabel.text = item
  return cell
}

dataSource.titleForHeaderInSection = { dataSource, index in
  return dataSource.sectionModels[index].header
}

Since binding table and collection views is an important everyday task, you’ll look into RxDataSources in more detail in a dedicated cookbook-style chapter later in this book.

RxAlamofire

RxAlamofire is a wrapper around the elegant Swift HTTP networking library Alamofire. Alamofire is one of the most popular third-party frameworks.

RxAlamofire features the following convenience extensions:

func data(_ method:_ url:parameters:encoding:headers:)
  -> Observable<Data>

This method combines all the request details into one call and returns the server response as Observable<Data>.

Further, the library offers:

func string(_ method:_ url:parameters:encoding:headers:)
  -> Observable<String>

This one returns an Observable of the content response as String.

Last, but no less important:

func json(_ method:_ url:parameters:encoding:headers:)
  -> Observable<Any>

This returns a JSON representation of an object using JSONSerialization.

Other than this, RxAlamofire also includes convenience functions to create observables to download or upload files and to retrieve progress information.

RxBluetoothKit

Working with Bluetooth can be complicated. Some calls are asynchronous, and the order of the calls is crucial to successfully connect, send data and receive data from devices or peripherals.

RxBluetoothKit abstracts some of the most painful parts of working with Bluetooth and delivers some cool features:

  • CBCentralManger support
  • CBPeripheral support
  • Scan sharing and queueing

To start using RxBluetoothKit, you have to create a manager:

let manager = CentralManager(queue: .main)

The code to scan for peripherals looks something along the lines of:

manager
  .scanForPeripherals(withServices: [serviceIds])
  .flatMap { scannedPeripheral in
    let advertisement = scannedPeripheral.advertisementData
    // Do whatever we want with the advertisement.
  }

And to connect to one:

manager.scanForPeripherals(withServices: [serviceId])
  .take(1)
  .flatMap { $0.peripheral.establishConnection() }
  .subscribe(onNext: { peripheral in
      print("Connected to: \(peripheral)")
  })

In addition to the manager, there are also super-convenient abstractions for characteristics and peripherals. For example, to connect to a peripheral you can do the following:

peripheral.establishConnection()
  .flatMap { $0.discoverServices([serviceId]) }
  .subscribe(onNext: { service in
      print("Service discovered: \(service)")
  })

And if you want to discover a characteristic:

peripheral.establishConnection()
  .flatMap { $0.discoverServices([serviceId]) }
  .flatMap { Observable.from($0) }
  .flatMap { $0.discoverCharacteristics([characteristicId])}
  .subscribe(onNext: { characteristic in
      print("Characteristic discovered: \(characteristic)")
  })

RxBluetoothKit also features functions to properly perform connection restorations, to monitor the state of Bluetooth and to monitor the connection state of single peripheral.

Challenge

Challenge: Add processing feedback

In this challenge you need to add some information about the processing of UIImages. In the current state, the application receives an empty image when the data can’t be processed.

Take a moment to review the code, remove the default, empty objects and make the code raise an error if the type conversion doesn’t work out. The RxURLSessionError enum in URLSession+Rx.swift already includes a case called deserializationFailed — throw it when type conversion fails.

Before starting, try to understand where this has to be raised and when. Sending an error to an observable is a termination, so make sure you are sending the error in the correct case.

If you can’t wrap up with this on your own, no worries — there’s a solution provided along with this chapter.

Where to go from here?

In this chapter, you saw how to implement and wrap an Apple framework. Sometimes, it’s very useful to abstract an official Apple Framework or third party library to better connect with RxSwift. There’s no real written rule about when an abstraction is necessary, but the recommendation is to apply this strategy if the framework meets one or more of these conditions:

  • Uses callbacks with completion and failure information.
  • Uses a lot of delegates to return information asynchronously.
  • Needs to inter-operate with other RxSwift parts of the application.

You also need to know if the framework has restrictions on which thread the data must be processed. For this reason, it’s a good idea to read the documentation thoroughly before creating a RxSwift wrapper.

And don’t forget to look for existing community extensions — or, if you’ve written one, consider sharing it back with the 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.