Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

4. Observables & Subjects in Practice
Written by Marin Todorov

By this point in the book, you understand how observables and different types of subjects work, and you’ve learned how to create and experiment with them in a Swift playground.

It could be a bit challenging, however, to see the practical use of observables in everyday development situations such as binding your UI to a data model, or presenting a new controller and getting output back from it.

It’s OK to be a little unsure how to apply these newly acquired skills to the real world. In this book, you’ll work through theoretical chapters such as Chapter 2, “Observables,” and Chapter 3, “Subjects”, as well as practical step-by-step chapters — just like this one!

In the “… in practice” chapters, you’ll work on a complete app. The starter Xcode project will include all the non-Rx code. Your task will be to add the RxSwift framework and add other features using your newly-acquired reactive skills.

That doesn’t mean to say you won’t learn a few new things along the way — au contraire!

In this chapter, you’ll use RxSwift and your new observable superpowers to create an app that lets users create nice photo collages — the reactive way.

Getting started

Open the starter project for this chapter: Combinestagram. It takes a couple of tries to roll your tongue just right to say the name, doesn’t it? It’s probably not the most marketable name, but it will do.

Install all pods and open Combinestagram.xcworkspace. Refer to Chapter 1, “Hello RxSwift,” for details on how to do that.

Select Assets/Main.storyboard and you’ll see the interface of the app you will bring to life:

In the first screen, the user can see the current photo collage and has buttons to either clear the current list of photos or to save the finished collage to disk. Additionally, when the user taps on the + button at the top-right, they will be taken to the second view controller in the storyboard where they will see the list of photos in their Camera Roll. The user can add photos to the collage by tapping on the thumbnails.

The view controllers and the storyboard are already wired up, and you can also peek at UIImage+Collage.swift to see how the actual collage is put together.

In this chapter, you are going to focus on putting your new skills to practice. Time to get started!

Using a subject/relay in a view controller

You’ll start by adding a BehaviorRelay<[UIImage]> property to the controller class and store the selected photos in its value. As you learned in Chapter 3, “Subjects”, the BehaviorRelay class works much like you’re used to with plain variables: you can manually change their value property any time you want. You will start with this simple example and later move on to subjects and custom observables.

Open MainViewController.swift and add the following inside the body of MainViewController:

private let bag = DisposeBag()
private let images = BehaviorRelay<[UIImage]>(value: [])

Since no other class will use those two constants, you define them as private. Encapsulation FTW!

The dispose bag is owned by the view controller. As soon as the view controller is released all your observable subscriptions will be disposed as well:

This makes Rx subscription memory management very easy: Simply throw subscriptions in the bag and they will be disposed alongside the view controller’s deallocation.

However, that won’t happen for this specific view controller, since it’s the root view controller and it isn’t released before the app quits. You’ll see the clever dispose-upon-deallocation mechanism at work later on in this chapter for the other controller in the storyboard.

At first, your app will always build a collage based on the same photo. No worries; it’s a nice photo from the Barcelona country side, which is already included in the app’s Asset Catalog. Each time the user taps +, you will add that same photo, one more time, to images.

Find actionAdd() and add the following to it:

let newImages = images.value
  + [UIImage(named: "IMG_1907.jpg")!]
images.accept(newImages)

First, you get the latest collection of images emitted by the relay fetching it via its value property and then you append one more image to it. Don’t mind the force-unwrapping after the UIImage initialization, we’re keeping things simple by skipping error handling for this chapter.

Next, you use the relay’s accept(_) to emit the updated set of images to any observers subscribed to the relay.

The initial value of the images relay is an empty array, and every time the user taps the + button, the observable sequence produced by images emits a new .next event with the new array as an element.

To permit the user to clear the current selection, scroll up and add the following to actionClear():

images.accept([])

With few lines of code in this chapter section, you neatly handled the user input. You can now move on to observing images and displaying the result on screen.

Adding photos to the collage

Now that you have images wired up, you can observe for changes and update the collage preview accordingly.

In viewDidLoad(), create the following subscription to images. Even though its a relay, you can subscribe to it directly, since its conforms to ObservableType, much like Observable itself does:

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

    preview.image = photos.collage(size: preview.frame.size)
  })
  .disposed(by: bag)

You subscribe for .next events emitted by images. For every event, you create a collage with the helper method collage(images:size:) provided for arrays of type UIImage. Finally, you add this subscription to the view controller’s dispose bag.

In this chapter, you are going to subscribe to your observables in viewDidLoad(). Later in the book, you will look into extracting these into separate classes and, in the last chapter, structure them into an MVVM architecture. You now have your collage UI together; the user can update images by tapping the + bar item (or Clear) and you update the UI in turn.

Run the app and give it a try! If you add the photo four times, your collage will look like this:

Wow, that was easy!

Of course, the app is a bit boring right now, but don’t worry — you will add the ability to select photos from Camera Roll in just a bit.

Driving a complex view controller UI

As you play with the current app, you’ll notice the UI could be a bit smarter to improve the user experience. For example:

  • You could disable the Clear button if there aren’t any photos selected just yet, or in the event the user has just cleared the selection.
  • Similarly, there’s no need for the Save button to be enabled if there aren’t any photos selected.
  • You could also disable Save for an odd number of photos, as that would leave an empty spot in the collage.
  • It would be nice to limit the amount of photos in a single collage to six, since more photos simply look a bit weird.
  • Finally, it would be nice if the view controller title reflected the current selection.

If you take a moment to read through the list above one more time, you’ll certainly see these modifications could be quite a hassle to implement the non-reactive way.

Thankfully, with RxSwift you simply subscribe to images one more time and update the UI from a single place in your code.

Add this subscription inside viewDidLoad():

images
  .subscribe(onNext: { [weak self] photos in
      self?.updateUI(photos: photos)
  })
  .disposed(by: bag)

Every time there’s a change to the photo selection, you call updateUI(photos:). You don’t have that method just yet, so add it anywhere inside the class body:

private func updateUI(photos: [UIImage]) {
  buttonSave.isEnabled = photos.count > 0 && photos.count % 2 == 0
  buttonClear.isEnabled = photos.count > 0
  itemAdd.isEnabled = photos.count < 6
  title = photos.count > 0 ? "\(photos.count) photos" : "Collage"
}

In the preceding code, you update the complete UI according to the ruleset above. All of the logic is in a single place and easy to read through. Run the app again, and you will see all the rules kick in as you play with the UI:

By now, you’re probably starting to see the real benefits of Rx when applied to your iOS apps. If you look through all the code you’ve written in this chapter, you’ll see there are only a few simple lines that drive the entire UI!

Talking to other view controllers via subjects

In this section of the chapter, you will connect the PhotosViewController class to the main view controller in order to let the user select arbitrary photos from their Camera Roll. That will result in far more interesting collages!

First, you need to push PhotosViewController to the navigation stack. Open MainViewController.swift and find actionAdd(). Comment out the existing code and add this code in its place:

let photosViewController = storyboard!.instantiateViewController(
  withIdentifier: "PhotosViewController") as! PhotosViewController

navigationController!.pushViewController(photosViewController, animated: true)

Here, you instantiate PhotosViewController from the project’s storyboard and push it onto the navigation stack. Run the app and tap + to see the Camera Roll.

The very first time you do this, you’ll need to grant access to your Photo Library:

Once you tap OK you will see what the photos controller looks like. The actual photos might differ on your device, and you might need to go back and try again after granting access.

The second time around, you should see the sample photos included with the iPhone Simulator.

If you were building an app using the established Cocoa patterns, your next step would be to add a delegate protocol so that the photos controller could talk back to your main controller (that is, the non-reactive way):

With RxSwift, however, you have a universal way to talk between any two classes — an Observable! There is no need to define a special protocol, because an Observable can deliver any kind of message to any one or more interested parties — the observers.

Creating an observable out of the selected photos

You’ll next add a subject to PhotosViewController that emits a .next event each time the user taps a photo from the Camera Roll.Open PhotosViewController.swift and add the following near the top:

import RxSwift

You’d like to add a PublishSubject to expose the selected photos, but you don’t want the subject publicly accessible, as that would allow other classes to call onNext(_) and make the subject emit values. You might want to do that elsewhere, but not in this case.

Add the following properties to PhotosViewController:

private let selectedPhotosSubject = PublishSubject<UIImage>()
var selectedPhotos: Observable<UIImage> {
  return selectedPhotosSubject.asObservable()
}

Here, you define both a private PublishSubject that will emit the selected photos and a public property named selectedPhotos that exposes the subject’s observable.

Subscribing to this property is how the main controller can observe the photo sequence, without being able to interfere with it.

PhotosViewController already contains the code to read photos from your Camera Roll and display them in a collection view. All you need to do is add the code to emit the selected photo when the user taps on a collection view cell.

Scroll down to collectionView(_:didSelectItemAt:). The code inside fetches the selected image and flashes the collection cell to give the user a bit of a visual feedback.

Next, imageManager.requestImage(...) gets the selected photo and gives you image and info parameters to work with in its completion closure. In that closure, you’d like to emit a .next event from selectedPhotosSubject.

Inside the closure, just after the guard statement, add:

if let isThumbnail = info[PHImageResultIsDegradedKey as NSString] as? Bool, !isThumbnail {
  self?.selectedPhotosSubject.onNext(image)
}

You use the info dictionary to check if the image is the thumbnail or the full version of the asset. imageManager.requestImage(...) will call that closure once for each size. In the event you receive the full-size image, you call onNext(_) on your subject and provide it with the full photo.

That’s all it takes to expose an observable sequence from one view controller to another. There’s no need for delegate protocols or any other shenanigans of that sort.

As a bonus, once you remove the protocols, the controllers relationship becomes very simple:

Observing the sequence of selected photos

Your next task is to return to MainViewController.swift and add the code to complete the last part of the schema above: namely, observing the selected photos sequence.

Find actionAdd() and add the following just before the line where you push the controller onto the navigation stack:

photosViewController.selectedPhotos
  .subscribe(
    onNext: { [weak self] newImage in

    },
    onDisposed: {
      print("Completed photo selection")
    }
  )
  .disposed(by: bag)

Before you push the controller, you subscribe for events on its selectedPhotos observable. You are interested in two events: .next, which means the user has tapped a photo, and also when the subscription is disposed. You’ll see why you need that in a moment.

Insert the following code inside the onNext closure to get everything working. It’s the same code you had before, but this time it adds the photo from Camera Roll:

guard let images = self?.images else { return }
images.accept(images.value + [newImage])

Run the app, select a few photos from your Camera Roll, and go back to see the result. Cool!

Disposing subscriptions — review

The code seemingly works as expected, but try the following: Add few photos to a collage, go back to the main screen and inspect the console.

Do you see a message saying, “Completed photo selection”? You added a print to your last subscription’s onDispose closure, but it never gets called! That means the subscription is never disposed and never frees its memory!

How so? You subscribe an observable sequence and throw it in the main screen’s dispose bag. This subscription (as discussed in previous chapters) will be disposed of either when the bag object is released, or when the sequence completes via an error or completed event.

Since you neither destroy the main view controller to release its bag property, nor complete the photos sequence, your subscription just hangs around for the lifetime of the app!

To give your observers some closure, you could emit a .completed event when that controller disappears from the screen. This would notify all observers that the subscription has completed to help with automatic disposal.

Open PhotosViewController.swift and add a call to your subject’s onComplete() method in the controller’s viewWillDisappear(_:):

selectedPhotosSubject.onCompleted()

Perfect! Now, you’re ready for the last part of this chapter: taking a plain old boring function and converting it into a super-awesome and fantastical reactive class.

Creating a custom observable

So far, you’ve tried BehaviorRelay, PublishSubject, and an Observable. To wrap up, you’ll create your own custom Observable and turn a plain old callback API into a reactive class. You’ll use the Photos framework to save the photo collage — and since you’re already an RxSwift veteran, you are going to do it the reactive way!

You could add a reactive extension on PHPhotoLibrary itself, but to keep things simple, in this chapter you will create a new custom class named PhotoWriter:

Creating an Observable to save a photo is easy: If the image is successfully written to disk you will emit its asset ID and a .completed event, or otherwise an .error event.

Wrapping an existing API

Open Classes/PhotoWriter.swift — this file includes a couple of definitions to get you started.

First, as always, add an import of the RxSwift framework:

import RxSwift

Then, add a new static method to PhotoWriter, which will create the observable you will give back to code that wants to save photos:

static func save(_ image: UIImage) -> Observable<String> {
  return Observable.create { observer in

  }
}

save(_:) will return an Observable<String>, because, after saving the photo, you will emit a single element: the unique local identifier of the created asset.

Observable.create(_) creates a new Observable, and you need to add all the meaty logic inside that last closure.

Add the following to the Observable.create(_) parameter closure:

var savedAssetId: String?
PHPhotoLibrary.shared().performChanges({

}, completionHandler: { success, error in

})

In the first closure parameter of performChanges(_:completionHandler:), you will create a photo asset out of the provided image; in the second one, you will emit either the asset ID or an .error event.

Add inside the first closure:

let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
savedAssetId = request.placeholderForCreatedAsset?.localIdentifier

You create a new photo asset by using PHAssetChangeRequest.creationRequestForAsset(from:) and store its identifier in savedAssetId. Next insert into completionHandler closure:

DispatchQueue.main.async {
  if success, let id = savedAssetId {
    observer.onNext(id)
    observer.onCompleted()
  } else {
    observer.onError(error ?? Errors.couldNotSavePhoto)
  }
}

If you got a success response back and savedAssetId contains a valid asset ID, you emit a .next event and a .completed event. In case of an error, you emit either a custom or the default error.

With that, your observable sequence logic is completed.

Xcode should already be warning you that you miss a return statement. As a last step, you need to return a Disposable out of that outer closure so add one final line to Observable.create({}):

return Disposables.create()

That wraps up the class nicely. The complete save() method should look like this:

static func save(_ image: UIImage) -> Observable<String> {
  return Observable.create({ observer in
    var savedAssetId: String?
    PHPhotoLibrary.shared().performChanges({
      let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
      savedAssetId = request.placeholderForCreatedAsset?.localIdentifier
    }, completionHandler: { success, error in
      DispatchQueue.main.async {
        if success, let id = savedAssetId {
          observer.onNext(id)
          observer.onCompleted()
        } else {
          observer.onError(error ?? Errors.couldNotSavePhoto)
        }
      }
    })
    return Disposables.create()
  })
}

If you’ve been paying attention, you might be asking yourself, “Why do we need an Observable that emits just a single .next event?”

Take a moment to reflect on what you’ve learned in the previous chapters. For example, you can create an Observable by using any of the following:

  • Observable.never(): Creates an observable sequences that never emits any elements.
  • Observable.just(_:): Emits one element and a .completed event.
  • Observable.empty(): Emits no elements followed by a .completed event.
  • Observable.error(_): Emits no elements and a single .error event.

As you see, observables can produce any combination of zero or more .next events, possibly terminated by either a .completed or an .error.

In the particular case of PhotoWriter, you are only interested in one event since the save operation completes just once. You use .next + .completed for successful writes, and .error if a particular write failed.

You get a big bonus point if you’re screaming “But what about Single?” about now. Indeed, what about Single?

RxSwift traits in practice

In Chapter 2, “Observables,” you had the chance to learn about RxSwift traits: specialized variations of the Observable implementation that are very handy in certain cases.

In this chapter, you’re going to do a quick review and use some of the traits in the Combinestagram project! Let’s start with Single.

Single

As you know from Chapter 2, Single is an Observable specialization. It represents a sequence, which can emit just once either a .success(Value) event or an .error. Under the hood, a .success is just .next + .completed pair.

This kind of trait is useful in situations such as saving a file, downloading a file, loading data from disk or basically any asynchronous operation that yields a value. You can categorize two distinct use-cases of Single:

  1. For wrapping operations that emit exactly one element upon success, just as PhotoWriter.save(_) earlier in this chapter.

    You can directly create a Single instead of an Observable. In fact you will update the save(_) method in PhotoWriter to create a Single in one of this chapter’s challenges.

  2. To better express your intention to consume a single element from a sequence and ensure if the sequence emits more than one element the subscription will error out.

    To achieve this, you can subscribe to any observable and use .asSingle() to convert it to a Single. You’ll try this just after you’ve finished reading through this section.

Maybe

Maybe is quite similar to Single with the only difference that the observable may not emit a value upon successful completion.

If we keep to the photograph-related examples imagine this use-case for Maybe, your app is storing photos in its own custom photo album. You persist the album identifier in UserDefaults and use that ID each time to “open” the album and write a photo inside. You would design a open(albumId:) -> Maybe<String> method to handle the following situations:

  • In case the album with the given ID still exists, just emit a .completed event.
  • In case the user has deleted the album in the meanwhile, create a new album and emit a .next event with the new ID so you can persist it in UserDefaults.
  • In case something is wrong and you can’t access the Photos library at all, emit an .error event.

Just like other traits, you can achieve the same functionality with using a “vanilla” Observable, but Maybe gives more context both to you as you’re writing your code and to the programmers coming to alter the code later on.

Just as with Single, you can either create a Maybe directly by using Maybe.create({ ... }) or by converting any observable sequence via .asMaybe().

Completable

The final trait to cover is Completable. This variation of Observable allows only for a single .completed or .error event to be emitted before the subscription is disposed of.

You can convert an observable sequence to a completable by using the ignoreElements() operator, in which case all next events will be ignored, with only a completed or error event emitted, just as required for a Completable.

You can also create a completable sequence by using Completable.create { ... } with code very similar to that you’d use to create other observables or traits.

You might notice that Completable simply doesn’t allow for emitting any values and wonder why would you need a sequence like that. You’d be surprised at the number of use-cases wherein you only need to know whether an async operation succeeded or not.

Let’s look at an example before going back to Combinestagram. Let’s say your app auto-saves the document while the user is working on it. You’d like to asynchronously save the document in a background queue and, when completed, show a small notification or an alert box onscreen if the operation fails.

Let’s say you wrapped the saving logic into a function saveDocument() -> Completable. This is how easy it is then to express the rest of the logic:

saveDocument()
  .andThen(Observable.from(createMessage))
  .subscribe(onNext: { message in
    message.display()
  }, onError: { e in
    alert(e.localizedDescription)
  })

The andThen operator allows you to chain more completables or observables upon a success event and subscribe for the final result. In case any of them emits an error, your code will fall through to the final onError closure.

I’ll assume you’re delighted to hear that you will get to use Completable in two chapters later in the book. And now back to Combinestagram and the problem at hand!

Subscribing to your custom observable

The current feature — saving a photo to the Photos library — falls under one of those special use-cases for which there is a special trait. Your PhotoWriter.save(_) observable emits just once (the new asset ID), or it errors out, and is therefore a great case for a Single.

Now for the sweetest part of all: making use of your custom-designed Observable and kicking serious butt along the way!

Open MainViewController.swift and add the following inside the actionSave() action method for the Save button:

guard let image = imagePreview.image else { return }

PhotoWriter.save(image)
  .asSingle()
  .subscribe(
    onSuccess: { [weak self] id in
      self?.showMessage("Saved with id: \(id)")
      self?.actionClear()
    },
    onError: { [weak self] error in
      self?.showMessage("Error", description: error.localizedDescription)
    }
  )
  .disposed(by: bag)

Above you call PhotoWriter.save(image) to save the current collage. Then you convert the returned Observable to a Single, ensuring your subscription will get at most one element, and display a message when it succeeds or errors out. Additionally, you clear the current collage if the write operation was a success.

Note: asSingle() ensures that you get at most one element by throwing an error if the source sequence emits more than one.

Give the app one last triumphant run, build up a nice photo collage and save it to the disk.

Don’t forget to check your Photos app for the result!

With that, you’ve completed Section 1 of this book — congratulations!

You are not a young Padawan anymore, but an experienced RxSwift Jedi. However, don’t be tempted to take on the Dark Side just yet. You will get to battle networking, thread switching, and error handling soon enough!

Before that, you must continue your training and learn about one of the most powerful aspects of RxSwift. In Section 2, “Operators and Best Practices,” operators will allow you to take your Observable superpowers to a whole new level!

Challenges

Before you move on to the next section, there are two challenges waiting for you. You will once again create a custom Observable — but this time with a little twist.

Challenge 1: It’s only logical to use a Single

You’ve probably noticed that you didn’t gain much by using .asSingle() when saving a photo to the Camera Roll. The observable sequence already emits at most one element!

Well, you are right about that, but the point was to provide a gentle introduction to .asSingle(). Now you can improve the code on your own in this very challenge.

Open PhotoWriter.swift and change the return type of save(_) to Single<String>. Then replace Observable.create with Single.create.

This should clear most errors. There is one last thing to take care of: Observable.create receives an observer as parameter so you can emit multiple values and/or terminating events. Single.create receives as a parameter a closure, which you can use only once to emit either a .success(T) or .error(E) values.

Complete the conversion yourself and remember that the parameter is a closure not an observer object, so you call it like this: single(.success(id)).

Challenge 2: Add custom observable to present alerts

Open MainViewController.swift and scroll towards the bottom of the file. Find the showMessage(_:description:) method that came with the starter project.

The method shows an alert onscreen and runs a callback when the user taps the Close button to dismiss the alert. That does sound quite similar to what you’ve already done for PHPhotoLibrary.performChanges(_), doesn’t it?

To complete this challenge, code the following:

  • Add an extension method to UIViewController that presents an alert onscreen with a given title and message and returns an Completable.
  • Add a Close button to allow the user to close the alert.
  • Dismiss the alert controller when the subscription is dismissed, so that you don’t have any dangling alerts.

In the end, use the new completable to present the alert from within showMessage(_:description:).

As always, if you run into trouble, or are curious to see the provided solution, you can check the completed project and challenge code in the projects folder for this chapter. You can peek in there anyway, but do give it your best shot first!

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.