Skip to content

Commit 5eefabf

Browse files
authored
Merge pull request #384 from ReactiveCocoa/readme-split
Break README into multiple documents.
2 parents 494b7b9 + 533fb76 commit 5eefabf

7 files changed

+427
-373
lines changed

.jazzy.yaml

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
custom_categories:
22
- name: Getting Started
33
children:
4+
- ReactivePrimitives
45
- BasicOperators
5-
- FrameworkOverview
6+
- Example.OnlineSearch
7+
- RxComparison
8+
- name: Advanced Topics
9+
children:
10+
- APIContracts
611
- DebuggingTechniques
7-
- DesignGuidelines
812
- name: Signal
913
children:
1014
- Signal
File renamed without changes.

Documentation/BasicOperators.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This document explains some of the most common operators used in ReactiveCocoa,
44
and includes examples demonstrating their use.
55

66
Note that “operators”, in this context, refers to functions that transform
7-
[signals][] and [signal producers][], _not_ custom Swift operators. In other
7+
[`Signal`s][Signal] and [`SignalProducer`s][SignalProducer], _not_ custom Swift operators. In other
88
words, these are composable primitives provided by ReactiveCocoa for working
99
with event streams.
1010

@@ -513,7 +513,7 @@ because some operators to [combine streams](#combining-event-streams) require
513513
the inputs to have matching error types.
514514

515515

516-
[Signals]: FrameworkOverview.md#signals
517-
[Signal Producers]: FrameworkOverview.md#signal-producers
516+
[Signal]: Documentation/ReactivePrimitives.md#signal-a-unidirectional-stream-of-events
517+
[SignalProducer]: Documentation/ReactivePrimitives.md#signalproducer-deferred-work-that-creates-a-stream-of-values
518518
[Observation]: FrameworkOverview.md#observation
519519

Documentation/Example.OnlineSearch.md

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Example: Online Searching
2+
3+
Let’s say you have a text field, and whenever the user types something into it,
4+
you want to make a network request which searches for that query.
5+
6+
_Please note that the following examples use Cocoa extensions in [ReactiveCocoa][] for illustration._
7+
8+
#### Observing text edits
9+
10+
The first step is to observe edits to the text field, using a RAC extension to
11+
`UITextField` specifically for this purpose:
12+
13+
```swift
14+
let searchStrings = textField.reactive.continuousTextValues
15+
```
16+
17+
This gives us a [Signal][] which sends values of type `String?`.
18+
19+
#### Making network requests
20+
21+
With each string, we want to execute a network request. ReactiveSwift offers an
22+
`URLSession` extension for doing exactly that:
23+
24+
```swift
25+
let searchResults = searchStrings
26+
.flatMap(.latest) { (query: String?) -> SignalProducer<(Data, URLResponse), AnyError> in
27+
let request = self.makeSearchRequest(escapedQuery: query)
28+
return URLSession.shared.reactive.data(with: request)
29+
}
30+
.map { (data, response) -> [SearchResult] in
31+
let string = String(data: data, encoding: .utf8)!
32+
return self.searchResults(fromJSONString: string)
33+
}
34+
.observe(on: UIScheduler())
35+
```
36+
37+
This has transformed our producer of `String`s into a producer of `Array`s
38+
containing the search results, which will be forwarded on the main thread
39+
(using the [`UIScheduler`][Schedulers]).
40+
41+
Additionally, [`flatMap(.latest)`][flatMapLatest] here ensures that _only one search_—the
42+
latest—is allowed to be running. If the user types another character while the
43+
network request is still in flight, it will be cancelled before starting a new
44+
one. Just think of how much code that would take to do by hand!
45+
46+
#### Receiving the results
47+
48+
Since the source of search strings is a `Signal` which has a hot signal semantic,
49+
the transformations we applied are automatically evaluated whenever new values are
50+
emitted from `searchStrings`.
51+
52+
Therefore, we can simply observe the signal using `Signal.observe(_:)`:
53+
54+
```swift
55+
searchResults.observe { event in
56+
switch event {
57+
case let .value(results):
58+
print("Search results: \(results)")
59+
60+
case let .failed(error):
61+
print("Search error: \(error)")
62+
63+
case .completed, .interrupted:
64+
break
65+
}
66+
}
67+
```
68+
69+
Here, we watch for the `Value` [event][Events], which contains our results, and
70+
just log them to the console. This could easily do something else instead, like
71+
update a table view or a label on screen.
72+
73+
#### Handling failures
74+
75+
In this example so far, any network error will generate a `Failed`
76+
[event][Events], which will terminate the event stream. Unfortunately, this
77+
means that future queries won’t even be attempted.
78+
79+
To remedy this, we need to decide what to do with failures that occur. The
80+
quickest solution would be to log them, then ignore them:
81+
82+
```swift
83+
.flatMap(.latest) { (query: String) -> SignalProducer<(Data, URLResponse), AnyError> in
84+
let request = self.makeSearchRequest(escapedQuery: query)
85+
86+
return URLSession.shared.reactive
87+
.data(with: request)
88+
.flatMapError { error in
89+
print("Network error occurred: \(error)")
90+
return SignalProducer.empty
91+
}
92+
}
93+
```
94+
95+
By replacing failures with the `empty` event stream, we’re able to effectively
96+
ignore them.
97+
98+
However, it’s probably more appropriate to retry at least a couple of times
99+
before giving up. Conveniently, there’s a [`retry`][retry] operator to do exactly that!
100+
101+
Our improved `searchResults` producer might look like this:
102+
103+
```swift
104+
let searchResults = searchStrings
105+
.flatMap(.latest) { (query: String) -> SignalProducer<(Data, URLResponse), AnyError> in
106+
let request = self.makeSearchRequest(escapedQuery: query)
107+
108+
return URLSession.shared.reactive
109+
.data(with: request)
110+
.retry(upTo: 2)
111+
.flatMapError { error in
112+
print("Network error occurred: \(error)")
113+
return SignalProducer.empty
114+
}
115+
}
116+
.map { (data, response) -> [SearchResult] in
117+
let string = String(data: data, encoding: .utf8)!
118+
return self.searchResults(fromJSONString: string)
119+
}
120+
.observe(on: UIScheduler())
121+
```
122+
123+
#### Throttling requests
124+
125+
Now, let’s say you only want to actually perform the search periodically,
126+
to minimize traffic.
127+
128+
ReactiveCocoa has a declarative `throttle` operator that we can apply to our
129+
search strings:
130+
131+
```swift
132+
let searchStrings = textField.reactive.continuousTextValues
133+
.throttle(0.5, on: QueueScheduler.main)
134+
```
135+
136+
This prevents values from being sent less than 0.5 seconds apart.
137+
138+
To do this manually would require significant state, and end up much harder to
139+
read! With ReactiveCocoa, we can use just one operator to incorporate _time_ into
140+
our event stream.
141+
142+
#### Debugging event streams
143+
144+
Due to its nature, a stream's stack trace might have dozens of frames, which, more often than not, can make debugging a very frustrating activity.
145+
A naive way of debugging, is by injecting side effects into the stream, like so:
146+
147+
```swift
148+
let searchString = textField.reactive.continuousTextValues
149+
.throttle(0.5, on: QueueScheduler.main)
150+
.on(event: { print ($0) }) // the side effect
151+
```
152+
153+
This will print the stream's [events][Events], while preserving the original stream behaviour. Both [`SignalProducer`][SignalProducer]
154+
and [`Signal`][Signal] provide the `logEvents` operator, that will do this automatically for you:
155+
156+
```swift
157+
let searchString = textField.reactive.continuousTextValues
158+
.throttle(0.5, on: QueueScheduler.main)
159+
.logEvents()
160+
```
161+
162+
For more information and advance usage, check the [Debugging Techniques][] document.
163+
164+
[SignalProducer]: Documentation/ReactivePrimitives.md#signalproducer-deferred-work-that-creates-a-stream-of-values
165+
[Schedulers]: Documentation/FrameworkOverview.md#Schedulers
166+
[Signal]: Documentation/ReactivePrimitives.md#signal-a-unidirectional-stream-of-events
167+
[Events]: Documentation/ReactivePrimitives.md#event-the-basic-transfer-unit-of-an-event-stream
168+
[Debugging Techniques]: Documentation/DebuggingTechniques.md
169+
170+
[retry]: Documentation/BasicOperators.md#retrying
171+
[flatMapLatest]: Documentation/BasicOperators.md#combining-latest-values
172+
173+
[ReactiveCocoa]: https://github.com/ReactiveCocoa/ReactiveCocoa/#readme

Documentation/ReactivePrimitives.md

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Core Reactive Primitives
2+
3+
1. [`Signal`](#signal-a-unidirectional-stream-of-events)
4+
1. [`Event`](#event-the-basic-transfer-unit-of-an-event-stream)
5+
1. [`SignalProducer`](#signalproducer-deferred-work-that-creates-a-stream-of-values)
6+
1. [`Property`](#property-an-observable-box-that-always-holds-a-value)
7+
1. [`Action`](#action-a-serialized-worker-with-a-preset-action)
8+
1. [`Lifetime`](#lifetime-limits-the-scope-of-an-observation)
9+
10+
#### `Signal`: a unidirectional stream of events.
11+
The owner of a `Signal` has unilateral control of the event stream. Observers may register their interests in the future events at any time, but the observation would have no side effect on the stream or its owner.
12+
13+
It is like a live TV feed — you can observe and react to the content, but you cannot have a side effect on the live feed or the TV station.
14+
15+
```swift
16+
let channel: Signal<Program, NoError> = tvStation.channelOne
17+
channel.observeValues { program in ... }
18+
```
19+
20+
*See also: [The `Signal` overview](FrameworkOverview.md#signals), [The `Signal` contract](APIContracts.md#the-signal-contract), [The `Signal` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Classes/Signal.html)*
21+
22+
23+
#### `Event`: the basic transfer unit of an event stream.
24+
A `Signal` may have any arbitrary number of events carrying a value, following by an eventual terminal event of a specific reason.
25+
26+
It is like a frame in a one-time live feed — seas of data frames carry the visual and audio data, but the feed would eventually be terminated with a special frame to indicate "end of stream".
27+
28+
*See also: [The `Event` overview](FrameworkOverview.md#events), [The `Signal` contract](APIContracts.md#the-event-contract), [The `Event` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Enums/Event.html)*
29+
30+
#### `SignalProducer`: deferred work that creates a stream of values.
31+
`SignalProducer` defers work — of which the output is represented as a stream of values — until it is started. For every invocation to start the `SignalProducer`, a new `Signal` is created and the deferred work is subsequently invoked.
32+
33+
It is like a on-demand streaming service — even though the episode is streamed like a live TV feed, you can choose what you watch, when to start watching and when to interrupt it.
34+
35+
36+
```swift
37+
let frames: SignalProducer<VideoFrame, ConnectionError> = vidStreamer.streamAsset(id: tvShowId)
38+
let interrupter = frames.start { frame in ... }
39+
interrupter.dispose()
40+
```
41+
42+
*See also: [The `SignalProducer` overview](FrameworkOverview.md#signal-producers), [The `SignalProducer` contract](APIContracts.md#the-signalproducer-contract), [The `SignalProducer` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Structs/SignalProducer.html)*
43+
44+
#### `Property`: an observable box that always holds a value.
45+
`Property` is a variable that can be observed for its changes. In other words, it is a stream of values with a stronger guarantee than `Signal` — the latest value is always available, and the stream would never fail.
46+
47+
It is like the continuously updated current time offset of a video playback — the playback is always at a certain time offset at any time, and it would be updated by the playback logic as the playback continues.
48+
49+
```swift
50+
let currentTime: Property<TimeInterval> = video.currentTime
51+
print("Current time offset: \(currentTime.value)")
52+
currentTime.signal.observeValues { timeBar.timeLabel.text = "\($0)" }
53+
```
54+
55+
*See also: [The `Property` overview](FrameworkOverview.md#properties), [The `Property` contract](APIContracts.md#the-property-contract), [The property API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Property.html)*
56+
57+
#### `Action`: a serialized worker with a preset action.
58+
When being invoked with an input, `Action` apply the input and the latest state to the preset action, and pushes the output to any interested parties.
59+
60+
It is like an automatic vending machine — after choosing an option with coins inserted, the machine would process the order and eventually output your wanted snack. Notice that the entire process is mutually exclusive — you cannot have the machine to serve two customers concurrently.
61+
62+
```swift
63+
// Purchase from the vending machine with a specific option.
64+
vendingMachine.purchase
65+
.apply(snackId)
66+
.startWithResult { result
67+
switch result {
68+
case let .success(snack):
69+
print("Snack: \(snack)")
70+
71+
case let .failure(error):
72+
// Out of stock? Insufficient fund?
73+
print("Transaction aborted: \(error)")
74+
}
75+
}
76+
77+
// The vending machine.
78+
class VendingMachine {
79+
let purchase: Action<Int, Snack, VendingMachineError>
80+
let coins: MutableProperty<Int>
81+
82+
// The vending machine is connected with a sales recorder.
83+
init(_ salesRecorder: SalesRecorder) {
84+
coins = MutableProperty(0)
85+
purchase = Action(state: coins, enabledIf: { $0 > 0 }) { coins, snackId in
86+
return SignalProducer { observer, _ in
87+
// The sales magic happens here.
88+
// Fetch a snack based on its id
89+
}
90+
}
91+
92+
// The sales recorders are notified for any successful sales.
93+
purchase.values.observeValues(salesRecorder.record)
94+
}
95+
}
96+
```
97+
98+
*See also: [The `Action` overview](FrameworkOverview.md#actions), [The `Action` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Classes/Action.html)*
99+
100+
#### `Lifetime`: limits the scope of an observation
101+
When observing a `Signal` or `SignalProducer`, it doesn't make sense to continue emitting values if there's no longer anyone observing them.
102+
Consider the video stream: once you stop watching the video, the stream can be automatically closed by providing a `Lifetime`:
103+
104+
```swift
105+
class VideoPlayer {
106+
private let (lifetime, token) = Lifetime.make()
107+
108+
func play() {
109+
let frames: SignalProducer<VideoFrame, ConnectionError> = ...
110+
frames.take(during: lifetime).start { frame in ... }
111+
}
112+
}
113+
```
114+
115+
*See also: [The `Lifetime` overview](FrameworkOverview.md#lifetimes), [The `Lifetime` API reference](http://reactivecocoa.io/reactiveswift/docs/latest/Classes/Lifetime.html)*

0 commit comments

Comments
 (0)