Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

7. Transforming Operators
Written by Scott Gardner

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Before you decided to buy this book and commit to learning RxSwift, you might have felt that RxSwift was some esoteric library; elusive, yet strangely compelling you to master it. And maybe that reminds you of when you first started learning iOS or Swift.

Now that you’re up to Chapter 7, you’ve come to realize that RxSwift isn’t magic. It’s a carefully constructed API that does a lot of heavy lifting for you and streamlines your code. You should be feeling good about what you’ve learned so far.

In this chapter, you’ll:

  • Learn about one of the most important categories of operators in RxSwift: transforming operators.
  • Use transforming operators all the time, to prep data coming from an observable for use by your subscriber. Once again, there are parallels between transforming operators in RxSwift and the Swift standard library, such as map(_:) and flatMap(_:).

By the end of this chapter, you’ll be transforming all the things!

Getting started

Run ./bootstrap.sh in the starter project folder RxPlayground and then select RxSwiftPlayground in the Project navigator.

Transforming elements

Observables emit elements individually, but you will frequently want to work with collections, such as when you’re binding an observable to a table or collection view, which you’ll learn how to do later in the book. A convenient way to transform an observable of individual elements into an array of all those elements is by using toArray.

example(of: "toArray") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of("A", "B", "C")
    // 2
    .toArray()
    .subscribe(onSuccess: {
      print($0)
    })
    .disposed(by: disposeBag)
}
--- Example of: toArray ---
["A", "B", "C"]

example(of: "map") {
  let disposeBag = DisposeBag()

  // 1
  let formatter = NumberFormatter()
  formatter.numberStyle = .spellOut

  // 2
  Observable<Int>.of(123, 4, 56)
    // 3
    .map {
      formatter.string(for: $0) ?? ""
    }
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}
example(of: "enumerated and map") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of(1, 2, 3, 4, 5, 6)
    // 2
    .enumerated()
    // 3
    .map { index, integer in
      index > 2 ? integer * 2 : integer
    }
    // 4
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}
--- Example of: enumerated and map ---
1
2
3
8
10
12
example(of: "compactMap") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of("To", "be", nil, "or", "not", "to", "be", nil)
    // 2
    .compactMap { $0 }
    // 3
    .toArray()
    // 4
    .map { $0.joined(separator: " ") }
    // 5
    .subscribe(onSuccess: {
      print($0)
    })
    .disposed(by: disposeBag)
}
--- Example of: compactMap ---
To be or not to be

Transforming inner observables

Add the following code to your playground, which you’ll use in the upcoming examples:

struct Student {
  let score: BehaviorSubject<Int>
}

example(of: "flatMap") {
  let disposeBag = DisposeBag()

  // 1
  let laura = Student(score: BehaviorSubject(value: 80))
  let charlotte = Student(score: BehaviorSubject(value: 90))

  // 2
  let student = PublishSubject<Student>()

  // 3
  student
    .flatMap {
      $0.score
    }
    // 4
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}
student.onNext(laura)
--- Example of: flatMap ---
80
laura.score.onNext(85)
85
student.onNext(charlotte)
90
laura.score.onNext(95)
95
charlotte.score.onNext(100)
100

example(of: "flatMapLatest") {
  let disposeBag = DisposeBag()

  let laura = Student(score: BehaviorSubject(value: 80))
  let charlotte = Student(score: BehaviorSubject(value: 90))

  let student = PublishSubject<Student>()

  student
    .flatMapLatest {
      $0.score
    }
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

  student.onNext(laura)
  laura.score.onNext(85)
  student.onNext(charlotte)

  // 1
  laura.score.onNext(95)
  charlotte.score.onNext(100)
}
--- Example of: flatMapLatest ---
80
85
90
100

Observing events

At times you may want to convert an observable into an observable of its events. One typical scenario where this is useful is when you do not have control over an observable that has observable properties, and you want to handle error events to avoid terminating outer sequences.

example(of: "materialize and dematerialize") {
  // 1
  enum MyError: Error {
    case anError
  }

  let disposeBag = DisposeBag()

  // 2
  let laura = Student(score: BehaviorSubject(value: 80))
  let charlotte = Student(score: BehaviorSubject(value: 100))

  let student = BehaviorSubject(value: laura)
}
// 1
let studentScore = student
  .flatMapLatest {
    $0.score
  }

// 2
studentScore
  .subscribe(onNext: {
    print($0)
  })
  .disposed(by: disposeBag)

  // 3
  laura.score.onNext(85)

  laura.score.onError(MyError.anError)

  laura.score.onNext(90)

  // 4
  student.onNext(charlotte)
--- Example of: materialize and dematerialize ---
80
85
Unhandled error happened: anError
 subscription called from:

let studentScore = student
  .flatMapLatest {
    $0.score.materialize()
  }
--- Example of: materialize and dematerialize ---
next(80)
next(85)
error(anError)
next(100)

studentScore
  // 1
  .filter {
    guard $0.error == nil else {
      print($0.error!)
      return false
    }

    return true
  }
  // 2
  .dematerialize()
  .subscribe(onNext: {
    print($0)
  })
  .disposed(by: disposeBag)
--- Example of: materialize and dematerialize ---
80
85
anError
100

Challenge

Completing challenges helps drive home what you learned in the chapter. There are starter and finished versions of the challenge in the exercise files download.

Challenge: Modify the challenge from Chapter 5 to take alpha-numeric characters

In Chapter 5’s challenge, you created a phone number lookup using filtering operators.

input
  .skipWhile { $0 == 0 }
  .filter { $0 < 10 }
  .take(10)
  .toArray()
  .subscribe(onNext: {
    let phone = phoneNumber(from: $0)
    if let contact = contacts[phone] {
       print("Dialing \(contact) (\(phone))...")
    } else {
       print("Contact not found")
    }
  })
  .disposed(by: disposeBag)
let convert: (String) -> Int? = { value in
  if let number = Int(value),
    number < 10 {
    return number
  }

  let convert: [String: Int] = [
    "abc": 2, "def": 3, "ghi": 4,
    "jkl": 5, "mno": 6, "pqrs": 7,
    "tuv": 8, "wxyz": 9
  ]

  let converted = keyMap
    .filter { $0.key.contains(value.lowercased()) }
    .map(\.value)
    .first

  return converted
}
let format: ([Int]) -> String = {
  var phone = $0.map(String.init).joined()

  phone.insert("-", at: phone.index(
    phone.startIndex,
    offsetBy: 3)
  )

  phone.insert("-", at: phone.index(
    phone.startIndex,
    offsetBy: 7)
  )

  return phone
}


let dial: (String) -> String = {
  if let contact = contacts[$0] {
    return "Dialing \(contact) (\($0))..."
  } else {
    return "Contact not found"
  }
}
Observable.of(1, 2, nil, 3)
  .filter { $0 != nil }
  .map { $0! }
  .subscribe(onNext: {
    print($0)
  })
  .disposed(by: disposeBag)
Observable.of(1, 2, nil, 3)
  .unwrap()
  .subscribe(onNext: {
    print($0)
  })
  .disposed(by: disposeBag)
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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now