Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

3. Subjects
Written by Scott Gardner

At this point, you know what an observable is, how to create one, how to subscribe to it, and how to dispose of things when you’re done. Observables are a fundamental part of RxSwift, but they’re essentially read-only. You may only subscribe to them to get notified of new events they produce.

A common need when developing apps is to manually add new values onto an observable during runtime to emit to subscribers. What you want is something that can act as both an observable and as an observer. That something is called a Subject.

In this chapter, you’ll learn about the different types of subjects in RxSwift, see how to work with each one and why you might choose one over another based on some common use cases. You’ll also learn about relays, which are wrappers around subjects. We’ll unwrap that later!

Getting started

Run ./bootstrap.sh in the starter project folder RxPlayground, which will open the project for this chapter, and select RxSwiftPlayground in the Project navigator. You’ll start out with a quick example to prime the pump. Add the following code to your playground:

example(of: "PublishSubject") {
  let subject = PublishSubject<String>()
}

You just created a PublishSubject. It’s aptly named, because, like a newspaper publisher, it will receive information and then publish it to subscribers. It’s of type String, so it can only receive and publish strings. After being initialized, it’s ready to receive strings.

Add the following code to your example:

subject.on(.next("Is anyone listening?"))

This puts a new string onto the subject. Nothing is printed out yet, because there are no observers. Create one by subscribing to the subject. Add the following code to the example:

let subscriptionOne = subject
  .subscribe(onNext: { string in
    print(string)
  })

You created a subscription to subject just like in the last chapter, printing next events. But still, nothing shows up in Xcode’s output console. What gives?

What’s happening here is that a PublishSubject only emits to current subscribers. So if you weren’t subscribed to it when an event was added to it, you won’t get it when you do subscribe. Think of the tree-falling-in-the-woods analogy. If a tree falls and no one’s there to hear it, does that make your illegal logging business a success? :]

To fix things, add this code to the end of the example:

subject.on(.next("1"))

Notice that, because you defined the publish subject to be of type String, only strings may be added to it. Now, because subject has a subscriber, it will emit the added value:

--- Example of: PublishSubject ---
1

In a similar fashion to the subscribe operators, on(.next(_:)) is how you add a new next event onto a subject, passing the element as the parameter. And just like subscribe, there’s shortcut syntax for subjects. Add the following code to the example:

subject.onNext("2")

onNext(_:) does the same thing as on(.next(_)). It’s just a bit easier on the eyes. And now the 2 is also printed:

--- Example of: PublishSubject ---
1
2

With that gentle intro, now it’s time to dig in and learn all about subjects.

What are subjects?

Subjects act as both an observable and an observer. You saw earlier how they can receive events and also be subscribed to. In the above example, the subject received next events, and for each of them, it turned around and emitted it to its subscriber.

There are four subject types in RxSwift:

  • PublishSubject: Starts empty and only emits new elements to subscribers.
  • BehaviorSubject: Starts with an initial value and replays it or the latest element to new subscribers.
  • ReplaySubject: Initialized with a buffer size and will maintain a buffer of elements up to that size and replay it to new subscribers.
  • AsyncSubject: Emits only the last next event in the sequence, and only when the subject receives a completed event. This is a seldom used kind of subject, and you won’t use it in this book. It’s listed here for the sake of completeness.

RxSwift also provides a concept called Relays. RxSwift provides two of these, named PublishRelay and BehaviorRelay. These wrap their respective subjects, but only accept and relay next events. You cannot add a completed or error event onto relays at all, so they’re great for non-terminating sequences.

Note: Did you notice the additional import RxRelay in this chapter’s playground? Originally, relays were part of RxCocoa, RxSwift’s suite of reactive Cocoa extensions and utilities. However, relays are a general-use concept that are also useful in non-Cocoa development environments such as Linux and command line tools. So it was split into its own consumable module, which RxCocoa depends on.

Next, you’ll learn more about these subjects and relays and how to work with them, starting with publish subjects.

Working with publish subjects

Publish subjects come in handy when you simply want subscribers to be notified of new events from the point at which they subscribed, until either they unsubscribe, or the subject has terminated with a completed or error event.

In the following marble diagram, the top line is the publish subject and the second and third lines are subscribers. The upward-pointing arrows indicate subscriptions, and the downward-pointing arrows represent emitted events.

The first subscriber subscribes after 1 is added to the subject, so it doesn’t receive that event. It does get 2 and 3, though. And because the second subscriber doesn’t join in until after 2 is added, it only gets 3.

Returning to the playground, add this code to the bottom of the same example:

let subscriptionTwo = subject
  .subscribe { event in
    print("2)", event.element ?? event)
  }

Events have an optional element property that contains the emitted element for next events. You use the nil-coalescing operator here to print the element if there is one; otherwise, you print the event.

As expected, subscriptionTwo doesn’t print anything out yet because it subscribed after the 1 and 2 were emitted. Now add this code:

subject.onNext("3")

The 3 is printed twice, once for subscriptionOne and once for subscriptionTwo.

3
2) 3

Add this code to terminate subscriptionOne and then add another next event onto the subject:

subscriptionOne.dispose()

subject.onNext("4")

The value 4 is only printed for subscription 2), because subscriptionOne was disposed.

2) 4

When a publish subject receives a completed or error event, also known as a stop event, it will emit that stop event to new subscribers and it will no longer emit next events. However, it will re-emit its stop event to future subscribers. Add this code to the example:

// 1
subject.onCompleted()
// 2
subject.onNext("5")

// 3
subscriptionTwo.dispose()

let disposeBag = DisposeBag()

// 4
subject
  .subscribe {
    print("3)", $0.element ?? $0)
  }
  .disposed(by: disposeBag)

subject.onNext("?")

From the top, you:

  1. Add a completed event onto the subject, using the convenience method for on(.completed). This terminates the subject’s observable sequence.
  2. Add another element onto the subject. This won’t be emitted and printed, though, because the subject has already terminated.
  3. Dispose of the subscription.
  4. Subscribe to the subject, this time adding its disposable to a dispose bag.

Maybe the new subscriber 3) will kickstart the subject back into action? Nope, but you do still get the completed event replayed.

2) completed
3) completed

Actually, subjects, once terminated, will re-emit their stop event to future subscribers. So it’s a good idea to include handlers for stop events in your code, not just to be notified when it terminates, but also in case it is already terminated when you subscribe to it. This can sometimes be the cause of subtle bugs, so watch out!

You might use a publish subject when you’re modeling time-sensitive data, such as in an online bidding app. It wouldn’t make sense to alert the user who joined at 10:01 am that at 9:59 am there was only 1 minute left in the auction. That is, of course, unless you like 1-star reviews of your bidding app.

Sometimes you want to let new subscribers know what was the latest emitted element, even though that element was emitted before the subscription. For that, you’ve got some options.

Publish subjects don’t replay values to new subscribers. This makes them a good choice to model events such as “user tapped something” or “notification just arrived.”

Working with behavior subjects

Behavior subjects work similarly to publish subjects, except they will replay the latest next event to new subscribers. Check out this marble diagram:

The first line at the top is the subject. The first subscriber on the second line down subscribes after 1 but before 2, so it receives 1 immediately upon subscription, and then 2 and 3 when they’re emitted by the subject. Similarly, the second subscriber subscribes after 2 but before 3, so it receives 2 immediately and then 3 when it’s emitted.

Add this code to your playground, after the last example:

// 1
enum MyError: Error {
  case anError
}

// 2
func print<T: CustomStringConvertible>(label: String, event: Event<T>) {
  print(label, (event.element ?? event.error) ?? event)
}

// 3
example(of: "BehaviorSubject") {
  // 4
  let subject = BehaviorSubject(value: "Initial value")
  let disposeBag = DisposeBag()
}

Here’s the play-by-play:

  1. Define an error type to use in upcoming examples.
  2. Expanding upon the use of the ternary operator in the previous example, you create a helper function to print the element if there is one, an error if there is one, or else the event itself. How convenient!
  3. Start a new example.
  4. Create a new BehaviorSubject instance. Its initializer takes an initial value.

Note: Because BehaviorSubject always emits its latest element, you can’t create one without providing an initial value. If you can’t provide an initial value at creation time, that probably means you need to use a PublishSubject instead, or model your element as an Optional.

Next, add the following code to the example:

subject
  .subscribe {
    print(label: "1)", event: $0)
  }
  .disposed(by: disposeBag)

You subscribe to the subject immediately after it was created. Because no other elements have been added to the subject, it replays its initial value to the subscriber.

--- Example of: BehaviorSubject ---
1) Initial value

Now, insert the following code right before the previous subscription code, but after the definition of the subject:

subject.onNext("X")

The X is printed, because now it’s the latest element when the subscription is made.

--- Example of: BehaviorSubject ---
1) X

Add the following code to the end of the example. But first, look it over and see if you can determine what will be printed:

// 1
subject.onError(MyError.anError)

// 2
subject
  .subscribe {
    print(label: "2)", event: $0)
  }
  .disposed(by: disposeBag)

With this code, you:

  1. Add an error event onto the subject.
  2. Create a new subscription to the subject.

This prints:

1) anError
2) anError

Did you figure out that the error event will be printed twice, once for each subscription? If so, right on!

Behavior subjects are useful when you want to pre-populate a view with the most recent data. For example, you could bind controls in a user profile screen to a behavior subject, so that the latest values can be used to pre-populate the display while the app fetches fresh data.

Behavior subjects replay their latest value to new subscribers. This makes them a good choice to model state such as “request is currently loading,” or “the time is now 9:41.”

What if you wanted to show more than the latest value? For example, on a search screen, you may want to show the most recent five search terms used. This is where replay subjects come in.

Working with replay subjects

Replay subjects will temporarily cache, or buffer, the latest elements they emit, up to a specified size of your choosing. They will then replay that buffer to new subscribers.

The following marble diagram depicts a replay subject with a buffer size of 2.

The first subscriber (middle line) is already subscribed to the replay subject (top line) so it gets elements as they’re emitted. The second subscriber (bottom line) subscribes after 2, so it gets 1 and 2 replayed to it.

Keep in mind, when using a replay subject, that this buffer is held in memory. You can definitely shoot yourself in the foot here, such as if you set a large buffer size for a replay subject of some type whose instances each take up a lot of memory, like images.

Another thing to watch out for is creating a replay subject of an array of items. Each emitted element will be an array, so the buffer size will buffer that many arrays. It would be easy to create memory pressure here if you’re not careful.

Add this new example to your playground:

example(of: "ReplaySubject") {
  // 1
  let subject = ReplaySubject<String>.create(bufferSize: 2)
  let disposeBag = DisposeBag()

  // 2
  subject.onNext("1")
  subject.onNext("2")
  subject.onNext("3")
  // 3
  subject
    .subscribe {
      print(label: "1)", event: $0)
    }
    .disposed(by: disposeBag)

  subject
    .subscribe {
      print(label: "2)", event: $0)
    }
    .disposed(by: disposeBag)
}

From the top, you:

  1. Create a new replay subject with a buffer size of 2. Replay subjects are initialized using the type method create(bufferSize:).
  2. Add three elements onto the subject.
  3. Create two subscriptions to the subject.

The latest two elements are replayed to both subscribers; 1 never gets emitted, because 2 and 3 are added onto the replay subject with a buffer size of 2 before anything subscribed to it.

--- Example of: ReplaySubject ---
1) 2
1) 3
2) 2
2) 3

Next, add the following code to the example:

subject.onNext("4")

subject
  .subscribe {
    print(label: "3)", event: $0)
  }
  .disposed(by: disposeBag)

With this code, you add another element onto the subject, and then create a new subscription to it.

The first two subscriptions will receive that element as normal because they were already subscribed when the new element was added to the subject, while the new third subscriber will get the last two buffered elements replayed to it.

1) 4
2) 4
3) 3
3) 4

You’re getting pretty good at this stuff by now, so there should be no surprises, here. But what would happen if you threw a wrench into the works? Add this line of code right after adding 4 onto the subject, before creating the third subscription:

subject.onError(MyError.anError)

This may surprise you. And if so, that’s OK. Life’s full of surprises. :]

1) 4
2) 4
1) anError
2) anError
3) 3
3) 4
3) anError

What’s going on, here? The replay subject is terminated with an error, which it will re-emit to new subscribers — you learned this earlier. But the buffer is also still hanging around, so it gets replayed to new subscribers as well, before the stop event is re-emitted.

Add this line of code immediately after adding the error:

subject.dispose()

By explicitly calling dispose() on the replay subject beforehand, new subscribers will only receive an error event indicating that the subject was already disposed.

3) Object `RxSwift...ReplayMany<Swift.String>` was already disposed.

Explicitly calling dispose() on a replay subject like this isn’t something you generally need to do. If you’ve added your subscriptions to a dispose bag, then everything will be disposed of and deallocated when the owner — such as a view controller or view model — is deallocated.

It’s just good to be aware of this little gotcha for those edge cases.

Note: In case you’re wondering what is a ReplayMany, it’s an internal type that is used to create replay subjects.

By using a publish, behavior, or replay subject, you should be able to model almost any need. There may be times, though, when you simply want to go old-school and ask an observable type, “Hey, what’s your current value?” Relays FTW here!

Working with relays

You learned earlier that a relay wraps a subject while maintaining its replay behavior. Unlike other subjects — and observables in general — you add a value onto a relay by using the accept(_:) method. In other words, you don’t use onNext(_:). This is because relays can only accept values, i.e., you cannot add an error or completed event onto them.

A PublishRelay wraps a PublishSubject and a BehaviorRelay wraps a BehaviorSubject. What sets relays apart from their wrapped subjects is that they are guaranteed to never terminate.

Add this new example to your playground:

example(of: "PublishRelay") {
  let relay = PublishRelay<String>()

  let disposeBag = DisposeBag()
}

Nothing new here versus creating a PublishSubject, except the name. However, in order to add a new value onto a publish relay, you use the accept(_:) method. Add this code to your example:

relay.accept("Knock knock, anyone home?")

There are no subscribers yet, so nothing is emitted. Create a subscriber and then add another value onto relay:

relay
  .subscribe(onNext: {
    print($0)
  })
  .disposed(by: disposeBag)

relay.accept("1")

The output is the same as if you’d created a publish subject instead of a relay:

--- Example of: PublishRelay ---
1

There is no way to add an error or completed event onto a relay. Any attempt to do so such as the following will generate a compiler error (don’t add this code to your playground, it won’t work):

relay.accept(MyError.anError)
relay.onCompleted()

Remember that publish relays wrap a publish subject and work just like them, except the accept part and that they will not terminate. How about something a little more interesting? Say hello to my little friend, BehaviorRelay.

Behavior relays also will not terminate with a completed or error event. Because it wraps a behavior subject, a behavior relay is created with an initial value, and it will replay its latest or initial value to new subscribers. A behavior relay’s special power is that you can ask it for its current value at any time. This feature bridges the imperative and reactive worlds in a useful way.

Add this new example to your playground:

example(of: "BehaviorRelay") {
  // 1
  let relay = BehaviorRelay(value: "Initial value")
  let disposeBag = DisposeBag()

  // 2
  relay.accept("New initial value")
  // 3
  relay
    .subscribe {
      print(label: "1)", event: $0)
    }
    .disposed(by: disposeBag)
}

Here’s what you’re doing this time:

  1. You create a behavior relay with an initial value. The relay’s type is inferred, but you could also explicitly declare the type as BehaviorRelay<String>(value: "Initial value").
  2. Add a new element onto the relay.
  3. Subscribe to the relay.

The subscription receives the latest value.

--- Example of: BehaviorRelay ---
1) New initial value

Next, add this code to the same example:

// 1
relay.accept("1")

// 2
relay
  .subscribe {
    print(label: "2)", event: $0)
  }
  .disposed(by: disposeBag)

// 3
relay.accept("2")

From the top:

  1. Add a new element onto the relay.
  2. Create a new subscription to the relay.
  3. Add another new element onto the relay.

The existing subscription 1) receives the new value 1 added onto the relay. The new subscription receives that same value when it subscribes, because it’s the latest value. And both subscriptions receive the 2 when it’s added onto the relay.

1) 1
2) 1
1) 2
2) 2

Finally, add the following piece of code to the last example:

print(relay.value)

Remember, behavior relays let you directly access their current value. In this case, the latest value added onto the relay is 2, so that’s what is printed to the console.

2

This is very helpful when bridging the imperative world with the reactive world. You’ll try this in the second challenge of this chapter.

Behavior relays are versatile. You can subscribe to them to be able to react whenever a new next event is emitted, just like any other subject. And they can accommodate one-off needs, such as when you just need to check the current value without subscribing to receive updates.

Challenges

Put your new super subject skills to the test by completing these challenges. There are starter and finished versions for each challenge in the exercise files.

Challenge 1: Create a blackjack card dealer using a publish subject

In the starter project, twist down the playground page and Sources folder in the Project navigator, and select the SupportCode.swift file. Review the helper code for this challenge, including a cards array that contains 52 tuples representing a standard deck of cards, cardString(for:) and point(for:) helper functions, and a HandError enumeration.

In the main playground page, add code right below the comment // Add code to update dealtHand here that will evaluate the result returned from calling points(for:), passing the hand array. If the result is greater than 21, add the error HandError.busted onto dealtHand with the points that caused the hand to bust. Otherwise, add hand onto dealtHand as a next event.

Also in the main playground page, add code right after the comment // Add subscription to dealtHand here to subscribe to dealtHand and handle next and error events. For next events, print a string containing the results returned from calling cardString(for:) and points(for:). For an error event, just print the error.

The call to deal(_:) currently passes 3, so three cards will be dealt each time you press the Execute Playground button in the bottom-left corner of Xcode. See how many times you go bust versus how many times you stay in the game. Are the odds stacked up against you at the casino or what?

The card emoji characters are pretty small when printed in the console. If you want to be able to make out what cards were dealt, you can temporarily increase the font size of the Executable Console Output for this challenge.

To do so, select Xcode/Preferences…/Fonts & Colors/Console, select Executable Console Output, and click the T button in the bottom-right to change it to a larger font, such as 48.

Challenge 2: Observe and check user session state using a behavior relay

Most apps involve keeping track of a user session, and a behavior relay can come in handy for such a need. You can subscribe to react to changes to the user session such as log in or log out, or just check the current value for one-off needs. In this challenge, you’re going to implement examples of both.

Review the setup code in the starter project. There are a couple enumerations to model UserSession and LoginError, and functions to logInWith(username:password:completion:), logOut(), and performActionRequiringLoggedInUser(_:). There is also a for-in loop that attempts to log in and perform an action using invalid and then valid login credentials.

There are four comments indicating where you should add the necessary code in order to complete this challenge. Good luck!

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.