Created
February 8, 2017 23:19
-
-
Save jklausa/d4c5eff34cb69ce74868f6136b3579f1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Reactive Cocoa 5 crash-course | |
##👋 | |
I sincerely hate most RAC/RAS/FRP/WTF/BBQ (MOAR three letter acronyms is what programming needs, definitely) that start with "oh, Reactive Cocoa is a framework for working with streams of values over time" or some other similar word-soup. | |
If you understand it already, I'm sure it makes sense, for newcomers it's absolutely horrible and impenetrable. | |
Hopefully this will be more digestible. | |
(If you have experience from RAC2/other functional reactive programming framework you can skip to the ""I know RAC 2 / I know what a Signal is already" mode" section.) | |
## So what is it then? | |
In simplest terms, reactive programming means you won't be writing a bunch of methods that set text on your labels based on what user has done. Instead, you'll set up a _binding_ between your label and some other object that will send out updated text. (Most commonly, this will be some sort of a `ViewModel`). | |
ReactiveCocoa helps you do this by exposing a bunch of reactive extensions for most commonly used `UIKit` controls. | |
It would look something like this: (in your `viewDidLoad(:)` method or similar) | |
```swift | |
label.reactive.text <~ viewModel.text | |
``` | |
You'd only have to set this binding once, and then every time the `viewModel.text` changes, your label would automagically update. | |
If the above example looks to beautiful to be true, well, it unfortunately is. | |
You'd expect the `viewModel.text` to be just a regular `String`, right? | |
It's actually a `Property<String>`. | |
## Wait, what? / "But it could've been a `NSString` in Reactive Cocoa 2!" | |
"ReactiveCocoa, the Objective-C years" had much more magic happening under the hood. You could've just written something like this: | |
```objc | |
RAC(self.label, text) = RACObserve(self.viewModel, text); | |
``` | |
where `self.viewModel.text` would just be a plain old regular `NSString *`. | |
This required some amount of rather dark magic and runtime hackery (`KVO`, mostly) that's mostly unavailable in Swift (at least for anything that's not a `NSObject` subclass). | |
`Property` does away with all that, at the expense of being less ergonomic to use (your `viewModel` can't longer just expose a bunch of primitive properties; and you have to type out `Property()` when initializing your model, etc). | |
I think it's a fair deal. | |
## So what's the deal with the squiggly arrow then? | |
I tried understanding what's going on here by reading few tutorials, but I always found them too high-level and impenetrable. | |
What made it click for me was: | |
1. looking at the reactive extensions for `UITextField` | |
2. seeing the `var continuousTextValues: Signal<String?, NoError>` property | |
3. brief fight with compiler | |
4. building something that basically boiled down to: | |
```swift | |
textField.reactive.continuousTextValues.observeValues { value in print(value ?? "") } | |
``` | |
After typing few words into the text field and observing the debugger came the enlightenment. | |
I'd strongly recommend you try the same. | |
You should basically see whatever you're typing into the textField printed in your console. | |
Without setting up any delegates, or `addTarget(_:action:forControlEvents)` calls. | |
Magic-ish! You might even be tempted to see that what you're seeing is a "stream of value over time". (i'm sorry i'm sorry i couldn't resist). | |
Then I tried doing something like this: | |
```swift | |
let signal = textField.reactive.continuousTextValues | |
signal.map { ($0 ?? "").uppercased }.observeValues { value in print(value) } | |
``` | |
That's where the second enlightenment came. | |
You might think it was the "omg you can call `map` on a `Signal`!", but it was actually something much more obvious: | |
`Signal`s are just objects. They can be passed around. You can store them in a variable. They can be a property on an object. (You'd think that one was obvious, _especially_ since I just used one on a `UITextField`, but brains, man...) | |
So, back to squiggly arrow? | |
It just observes the `Signal` on the right hand side and sets the value of the _reactive_ property on the left whenever the right hand side sends a value. | |
(note I said "_reactive property_" here — you can't bind a `Signal<String, NoError>` to just a regular `let foo: String`) | |
I hope you have a basic grasp of what a `Signal` is or does by know. | |
I'm sorry if you don't, hopefully those will be of service: | |
* [marisibrothers.com](http://www.marisibrothers.com/2016/07/introduction-to-reactivecocoa-4-part-1.html) | |
* [mfclarke.github.io](http://mfclarke.github.io/2016/04/23/introduction-to-reactive-cocoa-4/) | |
* [ReactiveSwift readme](https://github.com/ReactiveCocoa/ReactiveSwift) | |
* [ReactiveSwift overview](https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/FrameworkOverview.md) | |
(those talk about RAC4, not 5, but we're more interested in the _concepts_, not _API's_ here) | |
For nitpickers: | |
* "but you said the viewModel exposes a `Property`, not a `Signal`! | |
* "*well, actually* the `<~` takes anything conforming to `BindingSourceProtocol`" | |
I know and I'm sorry. I think the inaccuracies are worth it by making the _concept_ easier to grasp. Sorry. | |
## "I know RAC 2 / I know what a `Signal` is already": | |
### Signal and Signal Producer | |
`Signal`s are now two distinct types: `Signal` and `SignalProducer`, parametrized by two types: `Value` and `Error`. (i.e `Signal<String, MyCustomError>`). | |
`Signal` always sends values, no matter what. | |
`SignalProducer` is useful to indicate that the operation performs some work (i.e. network calls). When you _start_ it (`startWithSignal(:_)` / `startWithValues(:_)`), it _produces_ a ` Signal` you can observe to see the results of the work. | |
If your `Signal[Producer]` doesn't emit errors, specify `NoError`. (i.e. `Signal<String, NoError>`). | |
You might need to `import enum Result.NoError`. | |
### Properties | |
`Property` is like a `Signal`, except it _*always*_ has a current value. | |
They are interchangeable with `Signal`s and `SignalProducer`s when it comes to binding to a UI element. | |
They expose `var signal` and `var signalProducer` which do exactly what you'd expect based on the name. | |
There's also `MutableProperty` if you need to externally modify it. | |
You can compose properties: | |
```swift | |
let valid: Property<Bool> | |
let first: MutableProperty<String> | |
let last: MutableProperty<String> | |
/// (somewhere in the VC you'd have to hook them up, like: | |
/// viewModel.first <~ firstTextField.reactive.continuousTextValues.skipNil() | |
/// viewModel.last <~ lastTextField.reactive.continuousTextValues.skipNil() | |
let validClosure: (String, String) -> Bool = { !($0.isEmpty || $1.isEmpty) } | |
valid = Property(init: false, | |
then: first | |
.combineLatest(with: last) | |
.map(validClosure) | |
) | |
``` | |
Your `valid` is now externally immutable, but produces new values based on the `first` and `last` properties. | |
Note that UI elements here aren't the ultimate sources of truth here — the only thing they do is pipe whatever user put into them to the `viewModel`. | |
ReactiveCocoa encourages this style — UI controls don't expose `Properties` — only `Signal`s. | |
### Actions | |
I found it useful to think about `Action`s like `SignalProducer` factories. | |
`Action` with signature `Action<String, Int, NoError`> will spit out a `SignalProducer<Int, NoError>` after you call `apply(_:)` on it with a `String` as an argument. | |
Actions are sort of useful on their own, but the _cool shit_ starts after you bind them to a button. | |
Continuing on a previous example: | |
```swift | |
let action: Action<Void, NetworkEvent, NetworkErrors> | |
/// somewhere in the VC: | |
/// button.reactive.pressed = CocoaAction(viewModel.action) (note the `=` vs `<~`) | |
action = Action(state: first.combineLatest(with: last), | |
enabledIf: validClosure) { state, input in | |
SignalProducer { observer, disposable in | |
APIService.register(first: state.0, state.1) { success, error | |
guard success else { | |
observer.send(error: error) | |
} | |
observer.send(value: NetworkEvent.loggedIn) | |
observer.sendCompleted() | |
} | |
} | |
} | |
``` | |
Now, every time user presses the `button` the network connection would be kicked off. | |
`CocoaAction` is way cooler than that though, and it automagically binds the `button.reactive.isEnabled` property to the `viewModel.action.isEnabled` — so you get the validation for (sort-of) free. | |
We probably also want to handle errors. Helpfully there's a `errors: Signal<Error, Result.NoError>` property on `Action` (along with `events` and `values`). | |
So in your VC you'd do something like this: | |
```swift | |
vm.action.errors.observeValues { self.handle(error: $0) } | |
``` | |
If you need to know about specific values returned from the Action: | |
```swift | |
vm.action.errors.values | |
.filter { $0 == NetworkEvent.needsToAgreeToPrivacyPolicy } | |
.observeValues { [weak self] _ in | |
self.showPrivacyPolicy() | |
} | |
``` | |
Some notes to the above example: | |
* the filtering probably should be handled by the `ViewModel` itself by exposing a `showPrivacyPolicy` `Signal`, that you could react to in VC | |
* `Action`s that take `Void` as a input feel somewhat weird. We get away with it here, since all the state needed for the imaginary `register` action is contained within the viewModel itself. If this imaginary viewController had more buttons, you could expose one action that takes `enum ViewModelAction` as an input, and then switch over that to return an appropriate `SignalProducer` (so a different one for `login`/`register`/`logout` etc.) | |
* I barely have any idea what I'm doing here. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment