Skip to content

Commit

Permalink
Merge pull request #227 from DeclarativeHub/feature/align-with-combine
Browse files Browse the repository at this point in the history
Align some APIs with Combine
  • Loading branch information
srdanrasic authored Jun 22, 2019
2 parents 696aa5c + ef3aa40 commit 319ffc5
Show file tree
Hide file tree
Showing 28 changed files with 443 additions and 297 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ Signal(catching: {
//.observe { print($0) }

Signal<Int, NSError> { observer in
observer.next(1)
observer.next(2)
observer.completed()
observer.receive(1)
observer.receive(2)
observer.receive(completion: .finished)
return BlockDisposable {
print("disposed")
}
Expand Down
140 changes: 64 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,15 @@ ReactiveKit wraps the observer into a struct with various helper methods to make
```swift
/// Represents a type that receives events.
public protocol ObserverProtocol {

/// Type of elements being received.
associatedtype Element

/// Type of error that can be received.
associatedtype Error: Swift.Error

/// Send the event to the observer.
func on(_ event: Event<Element, Error>)
/// Type of elements being received.
associatedtype Element
/// Type of error that can be received.
associatedtype Error: Swift.Error

/// Send the event to the observer.
func on(_ event: Event<Element, Error>)
}
```

Expand All @@ -204,26 +204,26 @@ Our observer we introduced earlier is basically the `on(_:)` method. ReactiveKit
```swift
public extension ObserverProtocol {

/// Convenience method to send `.next` event.
public func next(_ element: Element) {
on(.next(element))
}

/// Convenience method to send `.failed` event.
public func failed(_ error: Error) {
on(.failed(error))
}
/// Convenience method to send `.next` event.
public func receive(_ element: Element) {
on(.next(element))
}

/// Convenience method to send `.completed` event.
public func completed() {
on(.completed)
}
/// Convenience method to send `.failed` or `.completed` event.
public func receive(completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
on(.completed)
case .failure(let error):
on(.failed(error))
}
}

/// Convenience method to send `.next` event followed by a `.completed` event.
public func completed(with element: Element) {
next(element)
completed()
}
/// Convenience method to send `.next` event followed by a `.completed` event.
public func receive(lastElement element: Element) {
receive(element)
receive(completion: .finished)
}
}
```

Expand All @@ -233,12 +233,12 @@ So with ReactiveKit we can implement previous example like this:
let counter = Signal<Int, Never> { observer in

// send first three positive integers
observer.next(1)
observer.next(2)
observer.next(3)
observer.receive(1)
observer.receive(2)
observer.receive(3)

// send completed event
observer.completed()
observer.receive(completion: .finished)
}
```

Expand Down Expand Up @@ -277,10 +277,10 @@ func getUser() -> Signal<User, ClientError> {
getUser(completion: { result in
switch result {
case .success(let user):
observer.next(user)
observer.completed()
observer.receive(user)
observer.receive(completion: .finished)
case .failure(let error):
observer.failed(error)
observer.receive(completion: .failure(error))
})
// return disposable, continue reading
}
Expand Down Expand Up @@ -385,10 +385,10 @@ func getUser() -> Signal<User, ClientError> {
let task = getUser(completion: { result in
switch result {
case .success(let user):
observer.next(user)
observer.completed()
observer.receive(user)
observer.receive(completion: .finished)
case .failure(let error):
observer.failed(error)
observer.receive(completion: .failure(error))
})

return BlockDisposable {
Expand Down Expand Up @@ -442,7 +442,7 @@ extension SignalProtocol {
switch event {
case .next(let element):
if isIncluded(element) {
observer.next(element)
observer.receive(element)
}
default:
observer(event)
Expand Down Expand Up @@ -519,14 +519,6 @@ let signal = Signal<Int, Never> { observer in

we will hit the wall because we cannot create an instance of `Never` so we cannot send `.failed` event. This is a very powerful and important feature because whenever you see a signal whose errors are specialized to `Never` type you can safely assume that signal will not fail - because it cannot.

Signals that do not fail are very common so ReactiveKit defines a typealias to make their usage simple and consistent.

```swift
public typealias SafeSignal<Element> = Signal<Element, Never>
```

You are recommend to use `SafeSignal` whenever you have a signal that cannot fail.

> Bindings work only with safe (non-failable) signals.


Expand All @@ -535,7 +527,7 @@ You are recommend to use `SafeSignal` whenever you have a signal that cannot fai
You will often need a signal that emits just one element and then completes. To make it, use static method `just`.

```swift
let signal = SafeSignal.just(5)
let signal = Signal<Int, Never>.just(5)
```

That will give you following signal:
Expand All @@ -547,7 +539,7 @@ That will give you following signal:
If you need a signal that fires multiple elements and then completes, you can convert any `Sequence` to a signal with static method `sequence`.

```swift
let signal = SafeSignal.sequence([1, 2, 3])
let signal = Signal<Int, Never>.sequence([1, 2, 3])
```
```
---1-2-3-|--->
Expand All @@ -557,7 +549,7 @@ To create a signal that just completes without sending any elements, do


```swift
let signal = SafeSignal<Int>.completed()
let signal = Signal<Int, Never>.completed()
```
```
---|--->
Expand All @@ -576,7 +568,7 @@ let signal = Signal<Int, MyError>.failed(MyError.someError)
You can also create a signal that never sends any events (i.e. a signal that never terminates).

```swift
let signal = SafeSignal.never()
let signal = Signal<Int, Never>.never()
```
```
------>
Expand All @@ -585,7 +577,7 @@ let signal = SafeSignal.never()
Sometimes you will need a signal that sends specific element after certain amount of time passes:

```swift
let signal = SafeSignal.timer(element: 5, time: 60)
let signal = Signal<Int, Never>(just: 5, after: 60)
```
```
---/60 seconds/---5-|-->
Expand All @@ -594,7 +586,7 @@ let signal = SafeSignal.timer(element: 5, time: 60)
Finally, when you need a signal that sends an integer every `interval` seconds, do

```swift
let signal = SafeSignal(sequence: 0..., interval: 5)
let signal = Signal<Int, Never>(sequence: 0..., interval: 5)
```
```
---0---1---2---3---...>
Expand Down Expand Up @@ -643,10 +635,10 @@ By default observers receive events on the thread or the queue where the event i
For example, if we have a signal that is created like

```swift
let someImage = SafeSignal<UIImage> { observer in
let someImage = Signal<UIImage, Never> { observer in
...
DispatchQueue.global(qos: .background).async {
observer.next(someImage)
observer.receive(someImage)
}
...
}
Expand Down Expand Up @@ -678,10 +670,10 @@ someImage
There is also another side to this. You might have a signal that does some slow synchronous work on whatever thread or queue it is observed on.

```swift
let someData = SafeSignal<Data> { observer in
let someData = Signal<Data, Never> { observer in
...
let data = // synchronously load large file
observer.next(data)
observer.receive(data)
...
}
```
Expand Down Expand Up @@ -717,7 +709,7 @@ Note that there are also operators `observeIn` and `executeIn`. Those operators
Bindings are observations with perks. Most of the time you should be able to replace observation with a binding. Consider the following example. Say we have a signal of users

```swift
let presentUserProfile: SafeSignal<User> = ...
let presentUserProfile: Signal<User, Never> = ...
```

and we would like to present a profile screen when a user is sent on the signal. Usually we would do something like:
Expand Down Expand Up @@ -754,7 +746,7 @@ Objects that conform to `Deallocatable` provide a signal that can tell us when t
public protocol Deallocatable: class {

/// A signal that fires `completed` event when the receiver is deallocated.
var deallocated: SafeSignal<Void> { get }
var deallocated: Signal<Void, Never> { get }
}
```

Expand All @@ -773,7 +765,7 @@ public protocol DisposeBagProvider: Deallocatable {

extension DisposeBagProvider {

public var deallocated: SafeSignal<Void> {
public var deallocated: Signal<Void, Never> {
return bag.deallocated
}
}
Expand Down Expand Up @@ -928,12 +920,12 @@ name.observeNext { name in print("Hi \(name)!") }
name.on(.next("Jim")) // prints: Hi Jim!

// ReactiveKit provides few extension toon the ObserverProtocol so we can also do:
name.next("Kathryn") // prints: Hi Kathryn!
name.send("Kathryn") // prints: Hi Kathryn!

name.completed()
name.send(completion: .finished)
```

> Note: When using ReactiveKit you should actually use `PublishSubject` instead. It has the same behaviour and interface as `Subject` we defined here - just a different name in order to be consistent with ReactiveX API.
> Note: When using ReactiveKit you should actually use `PassthroughSubject` instead. It has the same behaviour and interface as `Subject` we defined here - just a different name in order to be consistent with ReactiveX API.

As you can see, we do not have a producer closure, rather we send events to the subject itself. The subject then propagates those events to its own observers.

Expand All @@ -942,11 +934,11 @@ Subjects are useful when we need to convert actions from imperative world into s
```swift
class MyViewController: UIViewController {

fileprivate let _viewDidAppear = PublishSubject<Void, Never>()
fileprivate let _viewDidAppear = PassthroughSubject<Void, Never>()

override viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
_viewDidAppear.next()
_viewDidAppear.send()
}
}
```
Expand All @@ -957,7 +949,7 @@ We could have exposed subject publicly, but then anyone would be able to send ev
```swift
extension ReactiveExtensions where Base: MyViewController {

var viewDidAppear: SafeSignal<Void> {
var viewDidAppear: Signal<Void, Never> {
return base._viewDidAppear.toSignal() // convert Subject to Signal
}
}
Expand Down Expand Up @@ -1095,15 +1087,15 @@ Operator `retry` will only work sometimes and it will fail eventually. The resul
How do we convert failable signal into non-failable (safe) signal? We have to handle the error somehow. One way is to recover with a default element.

```swift
let image /*: SafeSignal<UIImage> */ = getImage().recover(with: .placeholder)
let image /*: Signal<UIImage, Never> */ = getImage().recover(with: .placeholder)
```

Now we get `SafeSignal` because the transformed signal will never fail. Any `.failed` event that might occur on original signal will just be replaced with `.next` event containing the default element (placeholder image in our example).
Now we get safe `Signal` because the transformed signal will never fail. Any `.failed` event that might occur on original signal will just be replaced with `.next` event containing the default element (placeholder image in our example).

Alternative way to get safe signal is to ignore - suppress - the error. You would do this if you really do not care about the error and nothing bad will happen if you ignore it.

```swift
let image /*: SafeSignal<UIImage> */ = getImage().suppressError(logging: true)
let image /*: Signal<UIImage, Never> */ = getImage().suppressError(logging: true)
```

It is always a good idea to log the error.
Expand All @@ -1118,7 +1110,7 @@ let image = getImage().flatMapError { error in

### Property

Property wraps mutable state into an object that enables observation of that state. Whenever the state changes, an observer will be notified. Just like the `PublishSubject`, it represents a bridge into the imperative world.
Property wraps mutable state into an object that enables observation of that state. Whenever the state changes, an observer will be notified. Just like the `PassthroughSubject`, it represents a bridge into the imperative world.

To create a property, just initialize it with the initial value.

Expand Down Expand Up @@ -1170,7 +1162,7 @@ public enum LoadingState<LoadingValue, LoadingError: Error>: LoadingStateProtoco
A signal with elements of `LoadingState` type is typealiased as `LoadingSignal`:

```swift
public typealias LoadingSignal<LoadingValue, LoadingError: Error> = SafeSignal<LoadingState<LoadingValue, LoadingError>>
public typealias LoadingSignal<LoadingValue, LoadingError: Error> = Signal<LoadingState<LoadingValue, LoadingError>, Never>
```

Notice that loading signal is a safe signal. Signal itself can never fail, but errors can be emitted as `.failed` loading state. This means that the error does not terminate the signal - new events can be received after the error.
Expand Down Expand Up @@ -1250,7 +1242,7 @@ fetchImage
}
```

Exciting! Operator `consumeLoadingState` takes the loading state listener and updates it each time a state is produced by the loading signal. It returns a safe signal of loading values, i.e. it unwraps the underlying value from the `.loaded` state. In our example that would be `SafeSignal<UIImage>` which we can then bind to our image view and update its content.
Exciting! Operator `consumeLoadingState` takes the loading state listener and updates it each time a state is produced by the loading signal. It returns a safe signal of loading values, i.e. it unwraps the underlying value from the `.loaded` state. In our example that would be `Signal<UIImage, Never>` which we can then bind to our image view and update its content.

#### Transforming loading signals

Expand Down Expand Up @@ -1296,7 +1288,7 @@ class UserService {
Say that you have a button that (re)loads a photo in your app. How to implement that in reactive world? First we will need a signal that represents buttons taps. With [Bond](https://github.com/DeclarativeHub/Bond) framework you can get that signal just like this:

```swift
let reload /*: SafeSignal<Void> */ = button.reactive.tap
let reload /*: Signal<Void, Never> */ = button.reactive.tap
```

The signal will send `.next` event whenever the button is tapped. We would like to load the photo on each such event. In order to do so, we will flat map the reload signal into photo requests.
Expand Down Expand Up @@ -1353,20 +1345,16 @@ or

## Installation

Bond framework is optional, but recommended for Cocoa / Cocoa touch development.

### Carthage

```
github "DeclarativeHub/ReactiveKit"
github "DeclarativeHub/Bond"
```

### CocoaPods

```
pod 'ReactiveKit'
pod 'Bond'
```

### Swift Package Manager
Expand Down
4 changes: 2 additions & 2 deletions ReactiveKit.podspec
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
Pod::Spec.new do |s|
s.name = "ReactiveKit"
s.version = "3.12.3"
s.version = "3.13.0"
s.summary = "A Swift Reactive Programming Framework"
s.description = "ReactiveKit is a Swift framework for reactive and functional reactive programming."
s.homepage = "https://github.com/DeclarativeHub/ReactiveKit"
s.license = 'MIT'
s.author = { "Srdan Rasic" => "srdan.rasic@gmail.com" }
s.source = { :git => "https://github.com/DeclarativeHub/ReactiveKit.git", :tag => "v3.12.3" }
s.source = { :git => "https://github.com/DeclarativeHub/ReactiveKit.git", :tag => "v3.13.0" }

s.ios.deployment_target = '8.0'
s.osx.deployment_target = '10.11'
Expand Down
Loading

0 comments on commit 319ffc5

Please sign in to comment.