Understanding the RxSwift Share Operator
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, but 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 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:
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 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.
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 right 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 the 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:
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 where 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 the .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:
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.
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
— reason why 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 likeshare(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 usingshare(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
:
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. Thus, I recommend playing around with those other examples.
Thank you for reading this — and find me on Twitter to let me know what you think!
Originally published at https://arctouch.com.