Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

2. Observables
Written by Scott Gardner

Now that you’ve learned some of the basic concepts of RxSwift, it’s time to take the jump and play with observables.

In this chapter, you’ll go over several examples of creating and subscribing to observables. The real-world use of some of the observables may seem a bit obscure, but rest assured that you’ll acquire important skills and learn a lot about the types of observables available to you in RxSwift. You’ll use these skills throughout the rest of this book — and beyond!

Getting started

For this chapter, you’re going to use an Xcode project that’s already been set up to include a playground and the RxSwift framework. To get started, open up the Terminal app, navigate to this chapter’s starter folder and then to the RxPlayground project folder. Finally, run the bootstrap.sh script by entering this command:

./bootstrap.sh

The bootstrap process will take a few seconds; remember that, every time you want to open this playground project, you will need to repeat the above steps. You cannot just open the playground file or workspace directly.

Select RxSwiftPlayground in the Project navigator, and you should see the following:

Twist down the playground page, through the Sources folder in the Project navigator, and select SupportCode.swift. It contains the following helper function example(of:):

public func example(of description: String, action: () -> Void) {
  print("\n--- Example of:", description, "---")
  action()
}

You’re going to use this function to encapsulate different examples as you work your way through this chapter. You’ll see how to use this function shortly.

But before you get too deep into that, now would probably be a good time to answer the question: What is an observable?

What is an observable?

Observables are the heart of Rx. You’re going to spend some time discussing what observables are, how to create them, and how to use them.

You’ll see “observable”, “observable sequence” and “sequence” used interchangeably in Rx. And, really, they’re all the same thing. You may even see an occasional “stream” thrown around from time to time, especially from developers that come to RxSwift from a different reactive programming environment. “Stream” also refers to the same thing, but, in RxSwift, all the cool kids call it a sequence, not a stream. In RxSwift…

…or something that works with a sequence. And an Observable is just a sequence, with some special powers. One of these powers — in fact the most important one — is that it is asynchronous. Observables produce events over a period of time, which is referred to as emitting. Events can contain values, such as numbers or instances of a custom type, or they can be recognized gestures, such as taps.

One of the best ways to conceptualize this is by using marble diagrams, which are just values plotted on a timeline.

The left-to-right arrow represents time, and the numbered circles represent elements of a sequence. Element 1 will be emitted, some time will pass, and then 2 and 3 will be emitted. How much time, you ask? It could be at any point throughout the lifetime of the observable — which brings you to the next topic you’ll learn about: the lifecycle of an observable.

Lifecycle of an observable

In the previous marble diagram, the observable emitted three elements. When an observable emits an element, it does so in what’s known as a next event.

Here’s another marble diagram, this time including a vertical bar that represents the end of the road for this observable.

This observable emits three tap events, and then it ends. This is called a completed event, that is, it’s terminated. For example, perhaps the taps were on a view that was dismissed. The important thing is that the observable is terminated, and can no longer emit anything. This is normal termination. However, sometimes things can go wrong.

An error occurred in this marble diagram, represented by the red X. The observable emitted an error event containing the error. This is the same as when an observable terminates normally with a completed event. If an observable emits an error event, it is also terminated and can no longer emit anything else.

Here’s a quick recap:

  • An observable emits next events that contain elements.
  • It can continue to do this until a terminating event is emitted, i.e., an error or completed event.
  • Once an observable is terminated, it can no longer emit events.

Events are represented as enumeration cases. Here’s the actual implementation in the RxSwift source code:

/// Represents a sequence event.
///
/// Sequence grammar:
/// **next\* (error | completed)**
public enum Event<Element> {
    /// Next element is produced.
    case next(Element)

    /// Sequence terminated with an error.
    case error(Swift.Error)

    /// Sequence completed successfully.
    case completed
}

Here, you see that next events contain an instance of some Element, error events contain an instance of Swift.Error and completed events are simply stop events that don’t contain any data.

Now that you understand what an observable is and what it does, you’ll create some observables to see them in action.

Creating observables

Switch back from the current file to RxSwiftPlayground and add the code below:

example(of: "just, of, from") {
  // 1
  let one = 1
  let two = 2
  let three = 3

  // 2
  let observable = Observable<Int>.just(one)
}

In the above code, you:

  1. Define integer constants you’ll use in the following examples.
  2. Create an observable sequence of type Int using the just method with the one integer constant.

The just method is aptly named, because all it does is create an observable sequence containing just a single element. It is a static method on Observable. However, in Rx, methods are referred to as “operators.”And the eagle-eyed among you can probably guess which operator you’re going to check out next.

Add this code into the same example:

let observable2 = Observable.of(one, two, three)

This time, you didn’t explicitly declare the type. You might think, because you give it several integers, the type is Observable of [Int].

However, Option-click on observable2 to show its inferred type, and you’ll see that it’s an Observable of Int, not an array:

That’s because the of operator has a variadic parameter, and Swift can infer the Observable’s type based on it.

Pass an array to of when you want to create an observable array. Add this code to the bottom of the example:

let observable3 = Observable.of([one, two, three])

Option-click on observable3 and you’ll see that it is indeed an Observable of [Int]. The just operator can also take an array as its single element, which may seem a little weird at first. However, it’s the array that is the single element, not its contents.

Another operator you can use to create observables is from. Add this code to the bottom of the example:

let observable4 = Observable.from([one, two, three])

The from operator creates an observable of individual elements from an array of typed elements. Option-click on observable4 and you’ll see that it is an Observable of Int, not [Int]. The from operator only takes an array.

Your console is probably looking quite bare at the moment. That’s because you haven’t printed anything except the example header. Time to change that by subscribing to observables.

Subscribing to observables

From your experience as an iOS developer, you are likely familiar with NotificationCenter; it broadcasts notifications to observers. However, these observed notifications are different than RxSwift Observables. Here’s an example of an observer of the UIKeyboardDidChangeFrame notification, with a handler as a trailing closure (don’t add this code to your playground):

let observer = NotificationCenter.default.addObserver(
  forName: UIResponder.keyboardDidChangeFrameNotification,
  object: nil,
  queue: nil) { notification in
  // Handle receiving notification
}

Subscribing to an RxSwift observable is fairly similar; you call observing an observable subscribing to it. So instead of addObserver(), you use subscribe(). Unlike NotificationCenter, where developers typically use only its .default singleton instance, each observable in Rx is different.

More importantly, an observable won’t send events, or perform any work, until it has a subscriber.

Remember, an observable is really a sequence definition, and subscribing to an observable is really more like calling next() on an Iterator in the Swift standard library (don’t add this code to your playground):

let sequence = 0..<3

var iterator = sequence.makeIterator()

while let n = iterator.next() {
  print(n)
}

/* Prints:
 0
 1
 2
 */

Subscribing to observables is more streamlined. You can also add handlers for each event type an observable can emit. Recall that an observable emits next, error and completed events. A next event passes the element being emitted to the handler, and an error event contains an error instance.

To see this in action, add this new example to your playground. Remember to insert each new example on its own, after the closing curly bracket of the previous example.

example(of: "subscribe") {
  let one = 1
  let two = 2
  let three = 3

  let observable = Observable.of(one, two, three)
}

This is similar to the previous example, except this time you’re using the of operator. Now add this code at the bottom of this example, to subscribe to the observable:

observable.subscribe { event in
  print(event)
}

Note: The Console should automatically appear whenever there is output, but you can manually show it by selecting View ▸ Debug Area ▸ Activate Console from the menu. This is where the print statements in the playground display their output.

Option-click on the subscribe operator, and observe it takes a closure parameter that receives an Event of type Int and doesn’t return anything, and subscribe returns a Disposable. You’ll learn about disposables shortly.

This subscription will print out each event emitted by observable:

--- Example of: subscribe ---
next(1)
next(2)
next(3)
completed

The observable emits a next event for each element, then emits a completed event and is terminated. When working with observables, you’ll usually be primarily interested in the elements emitted by next events, rather than the events themselves.

To see one way to access the elements directly, replace the subscribing code from above with the following code:

observable.subscribe { event in
  if let element = event.element {
    print(element)
  }
}

Event has an element property. It’s an optional value, because only next events have an element. So you use optional binding to unwrap the element if it’s not nil. Now only the elements are printed, not the events containing the elements, nor the completed event:

1
2
3

That’s a nice pattern, and it’s so frequently used that there’s a shortcut for it in RxSwift. There’s a subscribe operator for each type of event an observable emits: next, error and completed.

Replace the previous subscription code with this:

observable.subscribe(onNext: { element in
  print(element)
})

Note: If you have code completion suggestions turned on in Xcode preferences, you may be asked for handlers for the other events. Ignore these for now.

Now you’re only handling next event elements and ignoring everything else. The onNext closure receives the next event’s element as an argument, so you don’t have to manually extract it from the event like you did before.

Now you know how to create observable of one element and of many elements. But what about an observable of zero elements? The empty operator creates an empty observable sequence with zero elements; it will only emit a completed event.

Add this new example to your playground:

example(of: "empty") {
  let observable = Observable<Void>.empty()
}

An observable must be defined as a specific type if it cannot be inferred. So the type must be explicitly defined, because empty has nothing from which to infer the type. Void is typically used because nothing is going to be emitted.

Add this code to the example to subscribe to the empty observable:

observable.subscribe(
  // 1
  onNext: { element in
    print(element)
  },

  // 2
  onCompleted: {
    print("Completed")
  }
)

In the above code, you:

  1. Handle next events, just like you did in the previous example.
  2. Simply print a message, because a .completed event does not include an element.

In the console, you’ll see that empty only emits a .completed event:

--- Example of: empty ---
Completed

What use is an empty observable? They’re handy when you want to return an observable that immediately terminates or intentionally has zero values.

As opposed to the empty operator, the never operator creates an observable that doesn’t emit anything and never terminates. It can be use to represent an infinite duration. Add this example to your playground:

example(of: "never") {
  let observable = Observable<Void>.never()

  observable.subscribe(
    onNext: { element in
      print(element)
    },
    onCompleted: {
      print("Completed")
    }
  )
}

Nothing is printed, except for the example header. Not even "Completed". How do you know if this is even working? Hang on to that inquisitive spirit until the Challenges section.

So far, you’ve worked with observables of specific elements or values. However, it’s also possible to generate an observable from a range of values.

Add this example to your playground:

example(of: "range") {
  // 1
  let observable = Observable<Int>.range(start: 1, count: 10)

  observable
    .subscribe(onNext: { i in  
      // 2
      let n = Double(i)

      let fibonacci = Int(
        ((pow(1.61803, n) - pow(0.61803, n)) /
          2.23606).rounded()
      )

      print(fibonacci)
  })
}

What you just did:

  1. Create an observable using the range operator, which takes a start integer value and a count of sequential integers to generate.
  2. Calculate and print the nth Fibonacci number for each emitted element.

There’s actually a better place than in the onNext handler to put code that transforms the emitted element. You’ll learn about that in Chapter 7, “Transforming Operators.”

Except for the never() example, up to this point you’ve worked with observables that automatically emit a completed event and naturally terminate.

Doing so allowed you to focus on the mechanics of creating and subscribing to observables, but this brushed an important aspect of subscribing to observables under the carpet. It’s time to do some housekeeping before moving on.

Disposing and terminating

Remember that an observable doesn’t do anything until it receives a subscription. It’s the subscription that triggers an observable’s work, causing it to emit new events until an error or completed event terminates the observable. However, you can also manually cause an observable to terminate by canceling a subscription to it.

Add this new example to your playground:

example(of: "dispose") {
  // 1
  let observable = Observable.of("A", "B", "C")

  // 2
  let subscription = observable.subscribe { event in
    // 3
    print(event)
  }
}

Quite simply:

  1. Create an observable of strings.
  2. Subscribe to the observable, this time saving the returned Disposable as a local constant called subscription.
  3. Print each emitted event in the handler.

To explicitly cancel a subscription, call dispose() on it. After you cancel the subscription, or dispose of it, the observable in the current example will stop emitting events.

Add this code to the bottom of the example:

subscription.dispose()

Managing each subscription individually would be tedious, so RxSwift includes a DisposeBag type. A dispose bag holds disposables — typically added using the disposed(by:) method — and will call dispose() on each one when the dispose bag is about to be deallocated.

Add this new example to your playground:

example(of: "DisposeBag") {
  // 1
  let disposeBag = DisposeBag()

  // 2
  Observable.of("A", "B", "C")
    .subscribe { // 3
      print($0)
    }
    .disposed(by: disposeBag) // 4
}

Step-by-step, you:

  1. Create a dispose bag.
  2. Create an observable.
  3. Subscribe to the observable and print out the emitted events using the default argument name $0.
  4. Add the returned Disposable from subscribe to the dispose bag.

This is the pattern you’ll use most frequently: creating and subscribing to an observable, and immediately adding the subscription to a dispose bag.

Why bother with disposables at all?

If you forget to add a subscription to a dispose bag, or manually call dispose on it when you’re done with the subscription, or in some other way cause the observable to terminate at some point, you will probably leak memory.

Don’t worry if you forget; the Swift compiler should warn you about unused disposables.

In the previous examples, you created observables with specific next event elements. Using the create operator is another way to specify all the events an observable will emit to subscribers.

Add this new example to your playground:

example(of: "create") {
  let disposeBag = DisposeBag()
  Observable<String>.create { observer in

  }
}

The create operator takes a single parameter named subscribe. Its job is to provide the implementation of calling subscribe on the observable. In other words, it defines all the events that will be emitted to subscribers.

If you option-click on create right now you will not get the Quick Help documentation, because this code won’t compile yet. So here’s a preview:

The subscribe parameter is an escaping closure that takes an AnyObserver and returns a Disposable. AnyObserver is a generic type that facilitates adding values onto an observable sequence, which will then be emitted to subscribers.

Change the implementation of create to the following:

Observable<String>.create { observer in
  // 1
  observer.onNext("1")

  // 2
  observer.onCompleted()

  // 3
  observer.onNext("?")

  // 4
  return Disposables.create()
}

Here’s what you do with this code:

  1. Add a next event onto the observer. onNext(_:) is a convenience method for on(.next(_:)).
  2. Add a completed event onto the observer. Similarly, onCompleted is a convenience method for on(.completed).
  3. Add another next event onto the observer.
  4. Return a disposable, defining what happens when your observable is terminated or disposed of; in this case, no cleanup is needed so you return an empty disposable.

Note: The last step, returning a Disposable, may seem strange at first. Remember that subscribe operators must return a disposable representing the subscription, so you use Disposables.create() to create a disposable.

Do you think the second onNext element (?) could ever be emitted to subscribers? Why or why not?

To see if you guessed correctly, subscribe to the observable by adding the following code on the next line after the create implementation:

.subscribe(
  onNext: { print($0) },
  onError: { print($0) },
  onCompleted: { print("Completed") },
  onDisposed: { print("Disposed") }
)
.disposed(by: disposeBag)

You subscribed to the observable, and implemented all the handlers using default argument names for element and error arguments passed to the onNext and onError handlers, respectively. The result is, the first next event element, "Completed" and "Disposed" are printed out. The second next event is not printed because the observable emitted a completed event and terminated before it is added.

--- Example of: create ---
1
Completed
Disposed

What would happen if you add an error to the observer? Add this code at the top of the example to define an error type with a single case:

enum MyError: Error {
  case anError
}

Next, add the following line of code between the observer.onNext and observer.onCompleted calls:

observer.onError(MyError.anError)

Now the observable emits the error and then terminates:

--- Example of: create ---
1
anError
Disposed

What would happen if you did not add a completed or error event, and also didn’t add the subscription to disposeBag? Comment out the observer.onError, observer.onCompleted and disposed(by: disposeBag) lines of code to find out.

Here’s the complete implementation:

example(of: "create") {
  enum MyError: Error {
    case anError
  }

  let disposeBag = DisposeBag()

  Observable<String>.create { observer in
    // 1
    observer.onNext("1")

//    observer.onError(MyError.anError)

    // 2
//    observer.onCompleted()

    // 3
    observer.onNext("?")

    // 4
    return Disposables.create()
  }
  .subscribe(
    onNext: { print($0) },
    onError: { print($0) },
    onCompleted: { print("Completed") },
    onDisposed: { print("Disposed") }
  )
//  .disposed(by: disposeBag)
}

Congratulations, you’ve just leaked memory! The observable will never finish, and the disposable will never be disposed.

--- Example of: create ---
1
?

Feel free to uncomment the line that adds the completed event or the code that adds the subscription to the disposeBag if you just can’t stand to leave this example in a leaky state.

Creating observable factories

Rather than creating an observable that waits around for subscribers, it’s possible to create observable factories that vend a new observable to each subscriber.

Add this new example to your playground:

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

  // 1
  var flip = false

  // 2
  let factory: Observable<Int> = Observable.deferred {

    // 3
    flip.toggle()

    // 4
    if flip {
      return Observable.of(1, 2, 3)
    } else {
      return Observable.of(4, 5, 6)
    }
  }
}

From the top, you:

  1. Create a Bool flag to flip which observable to return.
  2. Create an observable of Int factory using the deferred operator.
  3. Toggle flip, which happens each time factory is subscribed to.
  4. Return different observables based on whether flip is true or false.

Externally, an observable factory is indistinguishable from a regular observable. Add this code to the bottom of the example to subscribe to factory four times:

for _ in 0...3 {
  factory.subscribe(onNext: {
    print($0, terminator: "")
  })
  .disposed(by: disposeBag)

  print()
}

Each time you subscribe to factory, you get the opposite observable. In other words, you get 123, then 456, and the pattern repeats each time a new subscription is created:

--- Example of: deferred ---
123
456
123
456

Using Traits

Traits are observables with a narrower set of behaviors than regular observables. Their use is optional; you can use a regular observable anywhere you might use a trait instead. Their purpose is to provide a way to more clearly convey your intent to readers of your code or consumers of your API. The context implied by using a trait can help make your code more intuitive.

There are three kinds of traits in RxSwift: Single, Maybe and Completable. Without knowing anything more about them yet, can you guess how each one is specialized?

Singles will emit either a success(value) or error(error) event. success(value) is actually a combination of the next and completed events. This is useful for one-time processes that will either succeed and yield a value or fail, such as when downloading data or loading it from disk.

A Completable will only emit a completed or error(error) event. It will not emit any values. You could use a completable when you only care that an operation completed successfully or failed, such as a file write.

Finally, Maybe is a mashup of a Single and Completable. It can either emit a success(value), completed or error(error). If you need to implement an operation that could either succeed or fail, and optionally return a value on success, then Maybe is your ticket.

You’ll have an opportunity to work more with traits in Chapter 4, “Observables & Subjects in Practice,” and beyond. For now, you’ll run through a basic example of using a single to load some text from a text file named Copyright.txt in the Resources folder for this playground — because who doesn’t love some legalese once in a while?

Add this example to your playground:

example(of: "Single") {
  // 1
  let disposeBag = DisposeBag()

  // 2
  enum FileReadError: Error {
    case fileNotFound, unreadable, encodingFailed
  }

  // 3
  func loadText(from name: String) -> Single<String> {
    // 4
    return Single.create { single in

    }
  }
}

In the above code, you:

  1. Create a dispose bag to use later.
  2. Define an Error enum to model some possible errors that can occur in reading data from a file on disk.
  3. Implement a function to load text from a file on disk that returns a Single.
  4. Create and return a Single.

Add this code inside the create closure to complete the implementation:

// 1
let disposable = Disposables.create()

// 2
guard let path = Bundle.main.path(forResource: name, ofType: "txt") else {
  single(.error(FileReadError.fileNotFound))
  return disposable
}

// 3
guard let data = FileManager.default.contents(atPath: path) else {
  single(.error(FileReadError.unreadable))
  return disposable
}

// 4
guard let contents = String(data: data, encoding: .utf8) else {
  single(.error(FileReadError.encodingFailed))
  return disposable
}

// 5
single(.success(contents))
return disposable

With this code, you:

  1. Create a Disposable, because the subscribe closure of create expects it as its return type.
  2. Get the path for the filename, or else add a file not found error onto the Single and return the disposable you created.
  3. Get the data from the file at that path, or add an unreadable error onto the Single and return the disposable.
  4. Convert the data to a string; otherwise, add an encoding failed error onto the Single and return the disposable. Starting to see a pattern here?
  5. Made it this far? Add the contents onto the Single as a success, and return the disposable.

Now you can put this function to work. Add this code to the example:

// 1
loadText(from: "Copyright")
// 2
  .subscribe {
    // 3
    switch $0 {
    case .success(let string):
      print(string)
    case .error(let error):
      print(error)
    }
  }
  .disposed(by: disposeBag)

Here, you:

  1. Call loadText(from:) and pass the root name of the text file.
  2. Subscribe to the Single it returns.
  3. Switch on the event and print the string if it was successful, or print the error if not.

You should see the text from the file printed to the console, which is the same as the copyright comment at the bottom of the playground:

--- Example of: Single ---
Copyright (c) 2020 Razeware LLC
...

Try changing the filename to something else, and you should see the file not found error printed instead.

Challenges

Practice makes permanent. By completing these challenges, you’ll practice what you’ve learned in this chapter and pick up a few more tidbits of knowledge about working with observables.

A starter playground workspace as well as the finished version of it are provided for each challenge. Enjoy!

Challenge 1: Perform side effects

In the never operator example earlier, nothing printed out. That was before you added your subscriptions to dispose bags. However, if you had added a subscription to a dispose bag, you also could’ve printed out a message in subscribe’s onDisposed handler.

There is another useful operator for when you want to do some side work that doesn’t affect the observable’s emitted events.

The do operator enables you to insert side effects; that is, handlers to do things that will not change the emitted event in any way. Instead, do will just pass the event through to the next operator in the chain. Unlike subscribe, do also includes an onSubscribe handler.

The method for using the do operator is do(onNext:onError:onCompleted:onSubscribe:onDispose). Each parameter is optional and defaults to nil, so you can provide handlers for just the events you’re interested in. Use Xcode’s autocompletion to get the closure parameters for each of the events.

To complete this challenge, insert the do operator in the never example using the onSubscribe handler. Feel free to include any of the other handlers; they work just like subscribe’s handlers do.

And while you’re at it, create a dispose bag and add the subscription to it.

Don’t forget you can always peek at the finished challenge playground for “inspiration.”

Challenge 2: Print debug info

Performing side effects is one way to debug your Rx code. But it turns out that there’s even a better utility for that purpose: the debug operator, which will print information about every event for an observable.

It has several optional parameters, but perhaps the most useful one let’s you include an identifier string that is also printed out. In complex Rx chains, where you might add debug calls in multiple places, this can really help differentiate the source of each printout.

Continuing to work in the playground from the previous challenge, complete this challenge by replacing the use of the do operator with debug and provide a string identifier to it as a parameter. Observe the debug output in Xcode’s console.

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.