Understanding the RxSwift share operator

by: | Jun 10, 2019

[Editor’s note: This is the second post in a two-part series about sharing subscriptions in RxSwift, designed to help developers learn how to use replay and share operators with RxSwift’s playground examples]

In Part 1 of this series, we explored RxSwift’s Connectable Observable sequences by detailing publish, replay and refCount operators. We also briefly discussed the share operator, and now we’ll go into a more extensive explanation by performing the same kind of analysis we did in Part 1. If you haven’t already, I’d recommend reading read Part 1 so you get to know the auxiliary functions and how the code examples are structured.

This series is written for developers with a basic understanding of Swift 5.0 and RxSwift 5.0.

The share operator

We use the share operator when we need the chain of operators not to re-execute upon every subscription. Thus, everything before share is executed only once, as depicted in Listing 1 and Output 1:

let observable = Observable<Int>.create { observer -> Disposable in
  print("Creating observable")
  DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1, execute: {
    observer.onNext(2)
  }
  return Disposables.create()
}

let doubleObservable = observable.map { value -> Int in
  print("Multiplying by 2")
  return value * 2
}
let doubleSharedObservable = doubleObservable.share()

doubleSharedObservable.printNext("Subscription A")
doubleSharedObservable.printNext("Subscription B")

Listing 1: Example of share operator.

Creating observable
Multiplying by 2
Subscription A -> 4
Subscription B -> 4

Output 1: Logs of Listing 1.

In the example, observable is created by emitting 2 after one second. The value is then mapped to 4 in the doubleObservable and the mapping becomes shared in doubleSharedObservable. A and B are subscribed to the shared observable, so the creation closure will execute only once, upon the first subscription. The mapping function will also execute once for each emitted value, no matter the number of subscribers. That behavior can be verified in Output 1, where both subscriptions print the emitted value 4, but Creating observable and Multiplying by 2 don’t print twice.

Notice this is pretty much the same behavior of publish. And if you’re curious, take a look at the source code of those operators to see that their implementations are very similar. Both are based on the multicast operator. This operator basically broadcasts emitted values through a subject, which is an entity that is at the same time an observable and an observer. You can find additional information about this topic in this RxSwift Medium post.

In (very) simple terms, share passes a ReplaySubject to multicast. Internally, the subject is subscribed to the source observable (the underlying subscription) and its emitted values are handed to the subject, which in turn passes the values to the actual subscribers. Since this is not really the focus of this post, I suggest that you dig into RxSwift’s source code if you want to know more about the details of share.

In the example above we used share with its default parameters, but its full signature is share(replay:scope:), which also gives us similar functionality to replay and refCount operators. We’ll examine how that works below.

Replaying elements in shared subscriptions

The replaying functionality of share differs from replay only in the resulting sequence’s type, which is a regular observable rather than the connectable observable returned by replay.

_ = printSecondBoundary()
let intSequence = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance).share(replay: 2)

_ = intSequence.printNext("Subscription A")
delay(3) { _ = intSequence.printNext("Subscription B") }

Listing 2: Example of share’s replay functionality.

Subscription A -> 0
----- t1 -----

Subscription A -> 1
----- t2 -----

Subscription B -> 0
Subscription B -> 1
Subscription A -> 2
Subscription B -> 2
----- t3 -----

Subscription A -> 3
Subscription B -> 3
----- t4 -----

Output 2: Results of Listing 2’s execution.

In Listing 2, we have A subscribing at t0 (before any delay) and printing the incrementing emitted values at t1 and t2. Since we’re using a buffer of size 2, when B subscribes just before t3, the last two emitted values — 0 and 1 — are passed to it. Notice that these values don’t mean that intSequence was recreated. Otherwise, the values would be emitted one second apart from each other. Thus, share emits buffered elements (if any) as soon as a subscription occurs. It’s also worth mentioning that replay parameter has a default value of 0 — which explains Listing 1’s behavior.

Controlling replay buffer with scopes

In the previous examples, we didn’t care much about the scope parameter. However, we used its default value of .whileConnected, which is one of the cases of the SubjectLifetimeScope enum. The other possibility is .forever.
The scope parameter dictates how share handles the lifetime of the cache it uses for replaying elements. In reality, this is related to how share constructs the underlying subject it passes to multicast. However, since we’re interested in describing behavior rather than implementation details, I will keep the term “cache” for simplicity. Now let’s look at Listing 3 and Output 3:

_ = printSecondBoundary()
let intSequence = Observable<Int>
  .interval(.seconds(1), scheduler: MainScheduler.instance)
  .share(replay: 2, scope: .whileConnected)

var subscriptionA = intSequence.printNext("Subscription A")
var subscriptionB: Disposable?
delay(3) { subscriptionB = intSequence.printNext("Subscription B") }
delay(4) { subscriptionA.dispose() }
delay(5) { subscriptionB?.dispose() }
delay(6) { _ = intSequence.printNext("Subscription C") }

Listing 3: Using .whileConnected scope.

Subscription A -> 0
----- t1 -----

Subscription A -> 1
----- t2 -----

Subscription B -> 0
Subscription B -> 1
Subscription A -> 2
Subscription B -> 2
----- t3 -----

Subscription B -> 3
----- t4 -----

----- t5 -----

----- t6 -----

Subscription C -> 0
----- t7 -----

Output 3: Output of Listing 3.

Listing 3 is actually an extension of Listing 2, where I disposed of the first two subscriptions and performed another one, C, later on. This is a situation when the differences between scopes become evident, as does the refCount‘s aspect of share.
Pay careful attention to Output 3 and you will notice that C prints values from 0 and one second after its subscription. That’s share’s refCount-like behavior causing intSequence to be recreated (share actually uses refCount internally). Thus, intSequence is created when A subscribes at t0 and recreated when C subscribes at t6, since all previous subscriptions had been disposed of at this point.
Also, did you notice that unlike B, no elements were replayed for C? Because we’re using .whileConnected scope, when all subscriptions are disposed of, share‘s internal cache gets cleared. The same doesn’t happen with .forever, where the cached elements are kept no matter the number of subscribers. This is what we get by simply changing the scope in Listing 3:

Subscription A -> 0
----- t1 -----

Subscription A -> 1
----- t2 -----

Subscription B -> 0
Subscription B -> 1
Subscription A -> 2
Subscription B -> 2
----- t3 -----

Subscription B -> 3
----- t4 -----

----- t5 -----

Subscription C -> 2
Subscription C -> 3
----- t6 -----

Subscription C -> 0
----- t7 -----

Output 4: Results of using .forever scope.

As expected, the divergence lies in t6. The last emitted values — 2 and 3 — are replayed upon C‘s subscription, even though the sequence was recreated and restarted counting from “0.”
Thus, the key difference between scopes becomes clear when the number of subscribers drops from 1 to 0. In .forever scope, share will keep the replay cache. In .whileConnected, it won’t.
In the vast majority of the cases, you’ll be using .whileConnected — it’s the default parameter. But if you really need to use .forever, be careful. Especially if you’re working with large objects, ask yourself if it’s really a good idea to keep these objects indefinitely cached in memory.

Legacy sharing options

If you’re familiar with RxSwift 3.x, you’ve probably seen shareReplay(_:) and shareReplayLatestWhileConnected(). Both are deprecated since RxSwift 4.x and now you should only be using the share(replay:scope:) we analyzed in this series. If you try to use one of those, Xcode will fire a very descriptive warning telling you how to replace them, but here’s a quick summary:

  • shareReplay(bufferSize) now behaves like share(replay: bufferSize, scope: .whileConnected). However, in RxSwift 3.x, shareReplay(_:) acted like using .forever scope. Thus, if you’re refactoring legacy code, think carefully about which scope to use.
  • shareReplayLatestWhileConnected() is the same as using share(replay: 1, scope: .whileConnected).

Bonus: RxCocoa’s shared sequences

While RxSwift encloses the reactive extensions for the Swift programming language, RxCocoa encompasses abstractions specifically for the Cocoa environment (iOS, tvOS, macOS and watchOS).
Traits are examples of these abstractions. They are essentially syntactic-sugared observables modified in a way that’s useful for specific scenarios. And when discussing shared sequences, two important traits quickly come to mind: Driver and Signal.
Both are type aliases of SharedSequence with specific sharing strategy. Driver behaves like share(replay: 1) and Signal like share(). Driver will replay elements, while Signal won’t.
Except for the sharing capability, Driver and Signal are pretty much the same. They both dispatch events to the main thread and they don’t error out. Thus, you can use them to bind events to UI components in an easy and safe way. Listing 4 depicts an example of Driver:

let dataObservable = Observable<String>.create { observer -> Disposable in
  print("Feching data...")
  DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2, execute: {
    observer.onNext("Some data from the internet")
  })
  return Disposables.create()
}

let dataDriver = dataObservable.asDriver(onErrorJustReturn: "")

let bag = DisposeBag()

dataDriver
  .drive(textField.rx.text)
  .disposed(by: bag)

dataDriver
  .drive(otherTextField.rx.text)
  .disposed(by: bag)

Listing 4: Example of Driver.

In the example, I am creating a Driver from a regular observable that simulates fetching data from the Internet. Since it’s a shared sequence, the data is fetched only once and the result is bound to the text fields. And even though the data is sent from a background thread, Driver enforces that they are delivered on the main thread. Finally, it doesn’t let any errors reach the components, in this case, by sending an empty string instead. Traits are so cool!

What’s next? Keep playing on RxSwift playgrounds

In this series, we discussed many operators related to sharing subscriptions in RxSwift. We also learned what connectable observable sequences are and how to buffer elements. And even though we left out operators such as multicast, we developed a great understanding about the most commonly used operators. Hopefully, you are now able to use them in the most everyday scenarios.
Keep in mind that this is based on RxSwift’s playground examples, where you can find other cool stuff that’s not limited to sharing subscriptions and connectable operators. I recommend playing around with those examples.

Thank you for reading this — and find me on Twitter to let me know what you think!