If you’re new to the Swift API and migrating from RAC 2, start with the 3.0 changes. This section only covers the differences between 3.0
and 4.0
.
Just like in RAC 3
, because Objective-C is still in widespread use, 99% of RAC 2.x
code will continue to work under RAC 4.0
without any changes. That is, RAC 2.x
primitives are still available in RAC 4.0
.
ReactiveCocoa 4.0
targets Xcode 7.2.x and Swift 2.1.x, and it supports iOS 8.0
, watchOS 2.0
, tvOS 9.0
and OS X 10.9
.
The biggest change from RAC 3
to RAC 4
is that Signal
and SignalProducer
operators are implemented as protocol extensions instead of global functions. This is similar to many of the collection protocol changes in the Swift 2
standard
library.
This enables chaining signal operators with normal dot-method calling syntax, which makes autocompleting operators a lot easier.
Previously the custom |>
was required to enable chaining global functions without a mess of nested calls and parenthesis.
/// RAC 3
signal
|> filter { $0 % 2 == 0 }
|> map { $0 * $0 }
|> observe { print($0) }
/// RAC 4
signal
.filter { $0 % 2 == 0 }
.map { $0 * $0 }
.observeNext { print($0) }
Additionally, this means that SignalProducer
operators are less “magic”. In RAC 3 the Signal
operators were implicitly lifted to work on SignalProducer
via |>
. This was a point of confusion for some, especially when browsing the
source looking for these operators. Now as protocol extensions, the SignalProducer
operators are explicitly implemented in terms of their Signal
counterpart when available.
As already alluded to above, the custom |>
operator for chaining signals has been removed. Instead standard method calling syntax is used for chaining operators.
The improvements to associated enum values in Swift 2
mean that Event
case no longer need to be Box
ed. In fact, the Box
dependency has been removed completely from RAC 4
.
The observe
and start
overloads taking next
, error
, etc. optional function parameters have been removed. They’ve been replaced with methods taking a single function with
the target Event
case — observeNext
, startWithNext
, and the same for failed
and completed
. See #2311 and #2318 for more details.
The try
and catch
operators were renamed because of the addition of the error handling keywords with the same name. They are now attempt
and flatMapError
respectively. Also, tryMap
was renamed to attemptMap
for consistency.
This fills a gap that was missing in RAC 3
. It’s a common pattern to have signals-of-signals or signals-of-producers.
The addition of flatten
and flatMap
over these makes it now possible to work with any combination of Signal
s and SignalProducer
s.
The Error
case of Event
has changed to Failed
. This aims to help clarify the terminating nature of failure/error events and puts them in the same tense as other terminating cases (Interrupted
and Completed
). Likewise, some operations and parameters have been renamed (e.g. Signal.observeError
is now Signal.observeFailed
, Observer.sendError
is now Observer.sendFailed
).
The generic parameters of Signal
, SignalProducer
, and other related types
have been renamed to Value
and Error
from T
and E
respectively. This
is in-line with changes to the standard library to give more descriptive names
to type parameters for increased clarity. This should have limited impact,
only affecting generic, custom signal/producer extensions.
There were some Signal
operators that were missing SignalProducer
equivalents:
takeUntil
combineLatestWith
sampleOn
takeUntilReplacement
zipWith
Signal.on
.Signal.merge(signals:)
.Signal.empty
.skipUntil
.replayLazily
(#2639).
This is in-line with changes to the standard library in Swift 2
.
MutableProperty
received 3 new methods, similar to those in Atomic
: modify
, swap
, and withValue
.
Additionally, all PropertyType
s now have a signal: Signal<T>
in addition to their existing producer: SignalProducer<T>
property.
Bag
and Atomic
are now public. These are useful when creating custom operators for RAC types.
In order to force users to think about the desired capacity, this no longer defaults to Int.max
. Prior to this change one could have inadvertently cached every value emitted by the SignalProducer
. This needs to be specified manually now.
It’s still recommended to use SignalProducer.buffer
or PropertyType
when buffering behavior is desired. However, when you need to compose an existing SignalProducer
to avoid duplicate side effects, this operator is now available.
The full semantics of the operator are documented in the code, and you can see #2639 for full details.
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
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 type NSError
.
If it is impossible for a signal to error out, use the built-in
NoError
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.
In addition to the Next
, Error
, and Completed
events that have always been
part of RAC, version 3.0 adds another terminating
event—called
Interrupted
—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
and SignalProducer
operators should handle any received
Interrupted
event by forwarding it to their own observers. This ensures that
interruption correctly propagates through the whole signal chain.
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 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 to Signal
s, because any RACSignal
subscription could potentially
involve side effects. To obtain a Signal
, use RACSignal.toSignalProducer
followed by SignalProducer.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 the
executing
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.
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
class, and “cold” signals have been
separated into their own type. This
reduces complexity by making it clear that no Signal
object can trigger side
effects when observed.
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 returned RACDisposable
.
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
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.
Instead of the ambiguously named RACCommand
, the Swift API offers the
Action
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 an Action
,
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)
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
and flatMap
.
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 with
flatMap(.Concat)
, which has the additional benefit of predictable behavior if
the input stream is refactored to have more values in the future.
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
replace most uses of the RACObserve()
and RAC()
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 in PropertyOf
(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 a DynamicProperty
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.
Since the Signal
type, like RACSubject
, 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)
The producer version of
Signal.pipe
,
the SignalProducer.buffer
method can replace replaying with
RACReplaySubject
or any of the -replay…
methods.
Conceptually, buffer
creates a (optionally bounded) queue for events, much
like RACReplaySubject
, and replays those events when new Signal
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)
})
RACMulticastConnection
and the -publish
and -multicast:
operators were
always poorly understood features of RAC 2. In RAC 3, thanks to the Signal
and
SignalProducer
split, the SignalProducer.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)
}
Disposables 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.
RAC 3 replaces the multipurpose RACScheduler
class with two protocols,
SchedulerType
and DateSchedulerType
, 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 with UIScheduler
and
QueueScheduler.mainQueueScheduler
. The UIScheduler
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.