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

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

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.

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 {

}

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.

func response(request: URLRequest) -> Observable<(HTTPURLResponse, Data)> {
  return Observable.create { observer in
    // content goes here
    return Disposables.create()
  }
}
let task = self.base.dataTask(with: request) { data, response, error in

}
task.resume()
return Disposables.create { task.cancel() }
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
}
observer.onNext((httpResponse, data))
observer.onCompleted()
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
  }
}
func string(request: URLRequest) -> Observable<String> {
  return data(request: request).map { data in
    return String(data: data, encoding: .utf8) ?? ""
  }
}
func json(request: URLRequest) -> Observable<Any> {
  return data(request: request).map { data in
    return try JSONSerialization.jsonObject(with: data)
  }
}
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)
  }
}
func image(request: URLRequest) -> Observable<UIImage> {
  return data(request: request).map { data in
    return UIImage(data: data) ?? UIImage()
  }
}

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.

private var internalCache = [String: Data]()
extension ObservableType where Element == (HTTPURLResponse, Data) {

}
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
  })
}
return response(request: request).cache().map { response, data -> Data in
  //...
}
if let url = request.url?.absoluteString,
   let data = internalCache[url] {
  return Observable.just(data)
}

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.

return URLSession.shared.rx
  .decodable(request: request, type: GiphySearchResponse.self)
  .map(\.data)
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)
disposable.dispose()
disposable = SingleAssignmentDisposable()

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.

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.

let result = try! observabe.toBlocking().first()
expect(result).first != 0
expect(observable) != 0
func firstOrNil() -> Element? {}
let obj = ["array": ["foo", "bar"], "foo": "bar"] as [String: AnyHashable]
func testData() {
  let observable = URLSession.shared.rx.data(request: self.request)
  expect(observable.toBlocking().firstOrNil()).toNot(beNil())
}

{"array":["foo","bar"],"foo":"bar"}
{"foo":"bar","array":["foo","bar"]}
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())
}
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
}
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
}

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.

RxDataSources

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

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

RxAlamofire

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

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

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.

let manager = CentralManager(queue: .main)
manager
  .scanForPeripherals(withServices: [serviceIds])
  .flatMap { scannedPeripheral in
    let advertisement = scannedPeripheral.advertisementData
    // Do whatever we want with the advertisement.
  }
manager.scanForPeripherals(withServices: [serviceId])
  .take(1)
  .flatMap { $0.peripheral.establishConnection() }
  .subscribe(onNext: { peripheral in
      print("Connected to: \(peripheral)")
  })
peripheral.establishConnection()
  .flatMap { $0.discoverServices([serviceId]) }
  .subscribe(onNext: { service in
      print("Service discovered: \(service)")
  })
peripheral.establishConnection()
  .flatMap { $0.discoverServices([serviceId]) }
  .flatMap { Observable.from($0) }
  .flatMap { $0.discoverCharacteristics([characteristicId])}
  .subscribe(onNext: { characteristic in
      print("Characteristic discovered: \(characteristic)")
  })

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.

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:

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