Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

6. Filtering Operators in Practice
Written by Marin Todorov

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 the previous chapter, you began your introduction to the functional aspect of RxSwift. The first batch of operators you learned about helped you filter the elements of an observable sequence.

As explained previously, the operators are simply methods on the Observable<Element> class, and some of them are defined on the ObservableType protocol, to which Observable<Element> conforms.

The operators operate on the elements of their Observable class and produce a new observable sequence as a result. This comes in handy because, as you saw previously, this allows you to chain operators, one after another, and perform several transformations in sequence:

The preceding diagram definitely looks great in theory. In this chapter, you’re going to try using the filtering operators in a real-life app. In fact, you are going to continue working on the Combinestagram app that you already know and love from Chapter 4, “Observables and Subjects in Practice.”

Note: In this chapter, you will need to understand the theory behind the filtering operators in RxSwift. If you haven’t worked through Chapter 5, “Filtering Operators,” do that first and then come back to the current chapter.

Without further ado, let’s have a look at putting filter, take, and company to work!

Improving the Combinestagram project

If you successfully completed the challenges from Chapter 4, “Observables and Subjects in Practice,” re-open the project and keep working on it. Otherwise, you can use the starter project provided for this chapter.

It’s important that you have a correct solution to the challenge in Chapter 4, since it plays a role in one of the tasks in this chapter. If you’re in doubt, just consult UIAlertViewController+Rx.swift in the provided starter project and compare it to your own solution.

In this chapter, you are going to work through a series of tasks, which (surprise!) will require you to use different filtering operators. You’ll use different ones and see how you can use counterparts like skip and take. You’ll also learn how to achieve similar effect by using different operators, and finally, you will take care of a few of the issues in the current Combinestagram project.

Note: Since this book has only covered a few operators so far, you will not write the “best possible” code. For this chapter, don’t worry about best practices or proper architecture yet, but instead focus on truly understanding how to use the filtering operators. In this book, you’re going to slowly build up towards writing good RxSwift code. It’s a process!

Refining the photos’ sequence

Currently the main screen of the app looks like this:

Sharing subscriptions

Is there anything wrong with calling subscribe(...) on the same observable multiple times? Turns out there might be!

let numbers = Observable<Int>.create { observer in
    let start = getStartNumber()
    observer.onNext(start)
    observer.onNext(start+1)
    observer.onNext(start+2)
    observer.onCompleted()
    return Disposables.create()
}
var start = 0
func getStartNumber() -> Int {
    start += 1
    return start
}
numbers
  .subscribe(
    onNext: { el in
      print("element [\(el)]")
    },
    onCompleted: {
      print("-------------")
    }
  )
element [1]
element [2]
element [3]
-------------
element [1]
element [2]
element [3]
-------------
element [2]
element [3]
element [4]
-------------
let newPhotos = photosViewController.selectedPhotos
  .share()

newPhotos
  [ here the existing code continues: .subscribe(...) ]

Ignoring all elements

You will start with the simplest filtering operator: the one that filters out all elements. No matter your value or type, ignoreElements() says “You shall not pass!”

newPhotos
  .ignoreElements()
  .subscribe(onCompleted: { [weak self] in
    self?.updateNavigationIcon()
  })
  .disposed(by: bag)
private func updateNavigationIcon() {
  let icon = imagePreview.image?
    .scaled(CGSize(width: 22, height: 22))
    .withRenderingMode(.alwaysOriginal)

  navigationItem.leftBarButtonItem = UIBarButtonItem(image: icon,
    style: .done, target: nil, action: nil)
}

Filtering elements you don’t need

Of course, as great as ignoreElements() is, sometimes you will need to ignore just some of the elements — not all of them.

newPhotos
  .filter { newImage in
    return newImage.size.width > newImage.size.height
  }
  [existing code .subscribe(...)]

Implementing a basic uniqueness filter

Combinestagram, in its current form, has another controversial feature: you can add the same photo more than once. That doesn’t make for very interesting collages, so in this section you’ll add some advanced filtering to prevent the user from adding the same photo multiple times.

private var imageCache = [Int]()
[existing .filter {newImage in ... ]
.filter { [weak self] newImage in
  let len = newImage.pngData()?.count ?? 0
  guard self?.imageCache.contains(len) == false else {
    return false
  }
  self?.imageCache.append(len)
  return true
}
[existing code .subscribe(...)]
imageCache = []

Keep taking elements while a condition is met

One of the “best” bugs in Combinestagram is that the + button is disabled if you add six photos, which prevents you from adding any more images. But if you are in the photos view controller, you can add as many as you wish. There ought to be a way to limit those, right?

newPhotos
  .takeWhile { [weak self] image in
    let count = self?.images.value.count ?? 0
    return count < 6
  }
  [existing code: filter {...}]

Improving the photo selector

In this section, you will move on to PhotosViewController.swift. First, you are going to build a new custom Observable, and then (surprise!) filter it in different ways to improve the user experience on that screen.

PHPhotoLibrary authorization observable

When you first ran Combinestagram, you had to grant it access to your photo library. Do you remember if the user experience was flawless in that moment? Probably not. You were probably overwhelmed at the time with operators, observable sequences, and the like.

import Foundation
import Photos
import RxSwift

extension PHPhotoLibrary {
  static var authorized: Observable<Bool> {
    return Observable.create { observer in

      return Disposables.create()
    }
  }
}

DispatchQueue.main.async {
  if authorizationStatus() == .authorized {
    observer.onNext(true)
    observer.onCompleted()
  } else {
    observer.onNext(false)
    requestAuthorization { newStatus in
      observer.onNext(newStatus == .authorized)
      observer.onCompleted()
    }
  }
}

Reload the photos collection when access is granted

You have two scenarios in which you end up having access to the photo library:

private let bag = DisposeBag()
let authorized = PHPhotoLibrary.authorized
  .share()
authorized
  .skipWhile { !$0 }
  .take(1)
  .subscribe(onNext: { [weak self] _ in
    self?.photos = PhotosViewController.loadPhotos()
    DispatchQueue.main.async {
      self?.collectionView?.reloadData()
    }
  })
  .disposed(by: bag)
requestAuthorization { newStatus in
  observer.onNext(newStatus == .authorized)
}

Display an error message if the user doesn’t grant access

So far, you have subscribed for the cases when the user has granted Combinestagram access to the photos library, but you don’t do anything when they simply deny the app that right.

authorized
  .skip(1)
  .takeLast(1)
  .filter { !$0 }
  .subscribe(onNext: { [weak self] _ in
    guard let errorMessage = self?.errorMessage else { return }
    DispatchQueue.main.async(execute: errorMessage)
  })
  .disposed(by: bag)
authorized
  .skip(1)
  .filter { !$0 }
authorized
  .takeLast(1)
  .filter { !$0 }
authorized
  .distinctUntilChanged()
  .takeLast(1)
  .filter { !$0 }
private func errorMessage() {
  alert(title: "No access to Camera Roll",
    text: "You can grant access to Combinestagram from the Settings app")
    .subscribe(onCompleted: { [weak self] in
      self?.dismiss(animated: true, completion: nil)
      _ = self?.navigationController?.popViewController(animated: true)
    })
    .disposed(by: bag)
}

Trying out time-based filter operators

You will learn more details about time-based operators in Chapter 11, “Time Based Operators”. However, some of those operators are also filtering operators. That’s why you are going to try using a couple of them in this chapter.

Completing a subscription after given time interval

Right now if the user has denied access to their photo library they see the No access alert box and they have to tap on Close to go back.

.asObservable()
.take(.seconds(5), scheduler: MainScheduler.instance)
[existing code: .subscribe(onCompleted: ...]

Using throttle to reduce work on subscriptions with high load

Sometimes you are only interested in the current element of a sequence, and consider any previous values to be useless. For a real-life example, switch to MainViewController.swift and find viewDidLoad().

images
  .subscribe(onNext: { [weak imagePreview] photos in
    guard let preview = imagePreview else { return }

    preview.image = photos.collage(size: preview.frame.size)
  })
.throttle(.milliseconds(500), scheduler: MainScheduler.instance)
[existing code: .subscribe(onNext: ...]

Challenge

Challenge: Combinestagram’s source code

Your challenge is to clean up the code in your project. For example, right in that last spot where you added code in MainViewController.swift’s viewDidLoad(), there are two subscriptions to the same observable. Clean that up by using a shared sequence.

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