ReactiveCocoa v3.0 Release Notes
-
ReactiveCocoa 3.0 includes the first official Swift API, which is intended to eventually supplant the Objective-C API entirely.
However, because migration is hard and time-consuming, and because Objective-C is still in widespread use, 99% of RAC 2.x code will continue to work under RAC 3.0 without any changes.
Since the 3.0 changes are entirely additive, this document will discuss how concepts from the Objective-C API map to the Swift API. For a complete diff of 👀 all changes, see the 3.0 pull request.
- Hot signals are now Signals
- Cold signals are now SignalProducers
- Commands are now Actions
- Flattening/merging, concatenating, and switching are now one operator
- Using PropertyType instead of RACObserve and RAC
- Using Signal.pipe instead of RACSubject
- Using SignalProducer.buffer instead of replaying
- Using startWithSignal instead of multicasting
➕ Additions
Parameterized types
🚦 Thanks to Swift, it is now possible to express the type of value that a signal can send. RAC also requires that the type of errors be specified.
🚦 For example,
Signal<Int, NSError>
is a signal that may send zero or more integers, and which may send an error of typeNSError
.🚦 If it is impossible for a signal to error out, use the built-in [
NoError
](ReactiveCocoa/Swift/Errors.swift) type (which can be referred to, but never created) to represent that 🚦 case—for example,Signal<String, NoError>
is a signal that may send zero or more strings, and which will not send an error under any circumstances.🚦 Together, these additions make it much simpler to reason about signal interactions, and protect against several kinds of common bugs that occurred in Objective-C.
Interrupted event
In addition to the
Next
,Error
, andCompleted
events that have always been part of RAC, version 3.0 adds another terminating event—calledInterrupted
—that is used to communicate cancellation.🚦 Now, whenever a producer is disposed of, one final
Interrupted
event will be sent to all consumers, giving them a chance to react to the cancellation.🚦 Similarly, observing a hot signal that has already terminated will immediately result in an
Interrupted
event, to clearly indicate that no further events are possible.This brings disposal semantics more in line with normal event delivery, where events propagate downstream from producers to consumers. The result is a simpler 🚦 model for reasoning about non-erroneous, yet unsuccessful, signal terminations.
Note: Custom
Signal
andSignalProducer
operators should handle any receivedInterrupted
event by forwarding it to their own observers. This ensures that 🚦 interruption correctly propagates through the whole signal chain.Objective-C bridging
👍 To support interoperation between the Objective-C APIs introduced in RAC 2 and the Swift APIs introduced in RAC 3, the framework offers [bridging functions](ReactiveCocoa/Swift/ObjectiveCBridging.swift) that can convert types back and forth between the two.
Because the APIs are based on fundamentally different designs, the conversion is not always one-to-one; however, every attempt has been made to faithfully translate the concepts between the two APIs (and languages).
Common conversions include:
- The
RACSignal.toSignalProducer
method †- Converts
RACSignal *
toSignalProducer<AnyObject?, NSError>
- Converts
- 🚦 The
toRACSignal()
function- Converts
SignalProducer<AnyObject?, ErrorType>
toRACSignal *
- Converts
Signal<AnyObject?, ErrorType>
toRACSignal *
- Converts
- The
RACCommand.toAction
method ‡- Converts
RACCommand *
toAction<AnyObject?, AnyObject?, NSError>
- Converts
- The
toRACCommand
function ‡- Converts
Action<AnyObject?, AnyObject?, ErrorType>
toRACCommand *
- Converts
† It is not possible (in the general case) to convert arbitrary
RACSignal
🚦 instances toSignal
s, because anyRACSignal
subscription could potentially 🚦 involve side effects. To obtain aSignal
, useRACSignal.toSignalProducer
🚦 followed bySignalProducer.start
, thereby making those side effects explicit.‡ Unfortunately, the
executing
properties of actions and commands are not 🔀 synchronized across the API bridge. To ensure consistency, only observe theexecuting
property from the base object (the one passed into the bridge, not ⚡️ retrieved from it), so updates occur no matter which object is used for execution.Replacements
🚦 Hot signals are now Signals
🚦 In the terminology of RAC 2, a “hot”
RACSignal
does not trigger any side effects 🚦 when a-subscribe…
method is called upon it. In other words, hot signals are entirely producer-driven and push-based, and consumers (subscribers) cannot have any effect on their lifetime.This pattern is useful for notifying observers about events that will occur no matter what. For example, a
loading
boolean might flip between true and false regardless of whether anything is observing it.Concretely, every
RACSubject
is a kind of hot signal, because the events being forwarded are not determined by the number of subscribers on the subject.🚦 In RAC 3, “hot” signals are now solely represented by the 🚦 [
Signal
](ReactiveCocoa/Swift/Signal.swift) class, and “cold” signals have been 🚦 separated into their own type. This ⬇️ reduces complexity by making it clear that noSignal
object can trigger side effects when observed.🚦 Cold signals are now SignalProducers
🚦 In the terminology of RAC 2, a “cold”
RACSignal
performs its work one time for every subscription. In other words, cold signals perform side effects when a-subscribe…
method is called upon them, and may be able to cancel in-progress work if-dispose
is called upon the returnedRACDisposable
.This pattern is broadly useful because it minimizes unnecessary work, and 👍 allows operators like
take
,retry
,concat
, etc. to manipulate when work is 🚦 started and cancelled. Cold signals are also similar to how futures and promises work, and can be 👉 useful for structuring asynchronous code (like network requests).🚦 In RAC 3, “cold” signals are now solely represented by the 🚦 [
SignalProducer
](ReactiveCocoa/Swift/SignalProducer.swift) class, which clearly indicates their relationship to “hot” 🚦 signals. As the name indicates, a signal producer is responsible for creating 🚦 a signal (when started), and can 🚦 perform work as part of that process—meanwhile, the signal can have any number of observers without any additional side effects.Commands are now Actions
Instead of the ambiguously named
RACCommand
, the Swift API offers the [Action
](ReactiveCocoa/Swift/Action.swift) type—named as such because it’s 💻 mainly useful in UI programming—to fulfill the same purpose.Like the rest of the Swift API, actions are parameterized by the types they use. An action must indicate the type of input it accepts, the type of output it produces, and what kinds of errors can occur (if any). This eliminates a few classes of type error, and clarifies intention.
Actions are also intended to be simpler overall than their predecessor:
- Unlike commands, actions are not bound to or dependent upon the main thread, making it easier to reason about when they can be executed and when they will generate notifications.
- Actions also only support serial execution, because concurrent execution
was a rarely used feature of
RACCommand
that added significant complexity to the interface and implementation.
Because actions are frequently used in conjunction with AppKit or UIKit, there is also a
CocoaAction
class that erases the type parameters of anAction
, 👍 allowing it to be used from Objective-C.As an example, an action can be wrapped and bound to
UIControl
like so:self.cocoaAction = CocoaAction(underlyingAction) self.button.addTarget(self.cocoaAction, action: CocoaAction.selector, forControlEvents: UIControlEvents.TouchUpInside)
🔀 Flattening/merging, concatenating, and switching are now one operator
🚦 RAC 2 offers several operators for transforming a signal-of-signals into one 🚦
RACSignal
, including:-flatten
-flattenMap:
+merge:
-concat
+concat:
-switchToLatest
Because
-flattenMap:
is the easiest to use, it was often incorrectly chosen even when concatenation or switching semantics are more appropriate.RAC 3 distills these concepts down into just two operators,
flatten
andflatMap
. Note that these do not have the same behavior as-flatten
and-flattenMap:
from RAC 2. Instead, both accept a “strategy” which determines how the producer-of-producers should be integrated, which can be one of:.Merge
, which is equivalent to RAC 2’s-flatten
or+merge:
.Concat
, which is equivalent to-concat
or+concat:
.Latest
, which is equivalent to-switchToLatest
This reduces the API surface area, and forces callers to consciously think about which strategy is most appropriate for a given use.
For streams of exactly one value, calls to
-flattenMap:
can be replaced withflatMap(.Concat)
, which has the additional benefit of predictable behavior if 🔨 the input stream is refactored to have more values in the future.Using PropertyType instead of RACObserve and RAC
📚 To be more Swift-like, RAC 3 de-emphasizes Key-Value Coding (KVC) 📚 and Key-Value Observing (KVO) in favor of a less “magical” representation for properties. The [
PropertyType
protocol and implementations](ReactiveCocoa/Swift/Property.swift) replace most uses of theRACObserve()
andRAC()
macros.For example,
MutableProperty
can be used to represent a property that can be bound to. If changes to that property should be visible to consumers, it can ➕ additionally be wrapped inPropertyOf
(to hide the mutable bits) and exposed publicly.If KVC or KVO is required by a specific API—for example, to observe changes to
NSOperation.executing
—RAC 3 offers aDynamicProperty
type that can wrap those key paths. Use this class with caution, though, as it can’t offer any type safety, and many APIs (especially in AppKit and UIKit) are not documented to be KVO-compliant.🚦 Using Signal.pipe instead of RACSubject
🚦 Since the
Signal
type, likeRACSubject
, is always “hot”, 🚦 there is a special class method for creating a controllable signal. The 🚦Signal.pipe
method can replace the use of subjects, and expresses intent 👍 better by separating the observing API from the sending API.🚦 To use a pipe, set up observers on the signal as desired, then send values to the sink:
let (signal, sink) = Signal<Int, NoError>.pipe() signal.observe(next: { value in print(value) }) // Prints each number sendNext(sink, 0) sendNext(sink, 1) sendNext(sink, 2)
🚦 Using SignalProducer.buffer instead of replaying
The producer version of 🚦
Signal.pipe
, 🚦 theSignalProducer.buffer
method can replace replaying withRACReplaySubject
or any of the-replay…
methods.Conceptually,
buffer
creates a (optionally bounded) queue for events, much 🚦 likeRACReplaySubject
, and replays those events when newSignal
s are created from the producer.🚦 For example, to replay the values of an existing
Signal
, it just needs to be fed into the write end of the buffer:let signal: Signal<Int, NoError> let (producer, sink) = SignalProducer<Int, NoError>.buffer() // Saves observed values in the buffer signal.observe(sink) // Prints each value buffered producer.start(next: { value in print(value) })
🚦 Using startWithSignal instead of multicasting
RACMulticastConnection
and the-publish
and-multicast:
operators were 🚦 always poorly understood features of RAC 2. In RAC 3, thanks to theSignal
and 🚦SignalProducer
split, theSignalProducer.startWithSignal
method can replace multicasting.🚦
startWithSignal
allows any number of observers to attach to the created signal before any work is begun—therefore, the work (and any side effects) still occurs just once, but the values can be distributed to multiple interested observers. This fulfills the same purpose of multicasting, in a much clearer and more tightly-scoped way.For example:
let producer = timer(5, onScheduler: QueueScheduler.mainQueueScheduler).take(3) // Starts just one timer, sending the dates to two different observers as they // are generated. producer.startWithSignal { signal, disposable in signal.observe(next: { date in print(date) }) signal.observe(someOtherObserver) }
Minor changes
Disposable changes
[Disposables](ReactiveCocoa/Swift/Disposable.swift) haven’t changed much overall in RAC 3, besides the addition of a protocol and minor naming tweaks.
The biggest change to be aware of is that setting
SerialDisposable.innerDisposable
will always dispose of the previous value, which helps prevent resource leaks or logic errors from forgetting to dispose manually.⏱ Scheduler changes
⏱ RAC 3 replaces the multipurpose
RACScheduler
class with two protocols, ⏱ [SchedulerType
andDateSchedulerType
](ReactiveCocoa/Swift/Scheduler.swift), with multiple implementations of each. ⏱ This design indicates and enforces the capabilities of each scheduler using the type system.⏱ In addition, the
mainThreadScheduler
has been replaced withUIScheduler
and ⏱QueueScheduler.mainQueueScheduler
. TheUIScheduler
type runs operations as 🔀 soon as possible on the main thread—even synchronously (if possible), thereby replacing RAC 2’s-performOnMainThread
operator—while ⏱QueueScheduler.mainQueueScheduler
will always enqueue work after the current ⏱ run loop iteration, and can be used to schedule work at a future date.📚 [
Signal
]: https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/FrameworkOverview.md#signals 📚 [SignalProducer
]: https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/FrameworkOverview.md#signal-producers 📚 [Action
]: https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/FrameworkOverview.md#actions 📚 [BindingTarget
]: https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/FrameworkOverview.md#binding-target