Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use StateRelay instead of Variable #82

Merged
merged 4 commits into from
Oct 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,13 @@ func transform(action: Observable<Action>) -> Observable<Action> {

### Global States

Unlike Redux, ReactorKit doesn't define a global app state. It means that you can use anything to manage a global state. You can use a `Variable`, a `PublishSubject` or even a reactor. ReactorKit doesn't force to have a global state so you can use ReactorKit in a specific feature in your application.
Unlike Redux, ReactorKit doesn't define a global app state. It means that you can use anything to manage a global state. You can use a `BehaviorSubject`, a `PublishSubject` or even a reactor. ReactorKit doesn't force to have a global state so you can use ReactorKit in a specific feature in your application.

There is no global state in the **Action → Mutation → State** flow. You should use `transform(mutation:)` to transform the global state to a mutation. Let's assume that we have a global `Variable` which stores the current authenticated user. If you'd like to emit a `Mutation.setUser(User?)` when the `currentUser` is changed, you can do as following:
There is no global state in the **Action → Mutation → State** flow. You should use `transform(mutation:)` to transform the global state to a mutation. Let's assume that we have a global `BehaviorSubject` which stores the current authenticated user. If you'd like to emit a `Mutation.setUser(User?)` when the `currentUser` is changed, you can do as following:


```swift
var currentUser: Variable<User> // global state
var currentUser: BehaviorSubject<User> // global state

func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
return Observable.merge(mutation, currentUser.map(Mutation.setUser))
Expand Down Expand Up @@ -273,7 +273,7 @@ A view can be tested with a *stub* reactor. A reactor has a property `stub` whic

```swift
var isEnabled: Bool { get set }
var state: Variable<Reactor.State> { get }
var state: StateRelay<Reactor.State> { get }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is the only one that StateRelay can be documented.

var action: ActionSubject<Reactor.Action> { get }
var actions: [Reactor.Action] { get } // recorded actions
```
Expand Down
52 changes: 52 additions & 0 deletions Sources/ReactorKit/StateRelay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// StateRelay.swift
// ReactorKit
//
// Created by tokijh on 05/10/2018.
//

import RxSwift

/// StateRelay is a wrapper for `BehaviorSubject`.
///
/// Unlike `BehaviorSubject` it can't terminate with error or completed.
public final class StateRelay<Element>: ObservableType {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add some unit tests on this class?

public typealias E = Element

private let _subject: BehaviorSubject<Element>

/// Accepts `event` and emits it to subscribers
public func accept(_ event: Element) {
_subject.onNext(event)
}

/// Gets or sets current value of behavior subject
///
/// Whenever a new value is set, all the observers are notified of the change.
///
/// Even if the newly set value is same as the old value, observers are still notified for change.
public var value: Element {
get {
// this try! is ok because subject can't error out or be disposed
return try! _subject.value()
}
set(newValue) {
accept(newValue)
}
}

/// Initializes behavior relay with initial value.
public init(value: Element) {
_subject = BehaviorSubject(value: value)
}

/// Subscribes observer
public func subscribe<O: ObserverType>(_ observer: O) -> Disposable where O.E == E {
return _subject.subscribe(observer)
}

/// - returns: Canonical interface for push style sequence
public func asObservable() -> Observable<Element> {
return _subject.asObservable()
}
}
4 changes: 2 additions & 2 deletions Sources/ReactorKit/Stub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ public class Stub<Reactor: _Reactor> {

public var isEnabled: Bool = false

public let state: Variable<Reactor.State>
public let state: StateRelay<Reactor.State>
public let action: ActionSubject<Reactor.Action>
public private(set) var actions: [Reactor.Action] = []

public init(reactor: Reactor, disposeBag: DisposeBag) {
self.reactor = reactor
self.disposeBag = disposeBag
self.state = .init(reactor.initialState)
self.state = .init(value: reactor.initialState)
self.state.asObservable()
.subscribe(onNext: { [weak reactor] state in
reactor?.currentState = state
Expand Down
127 changes: 127 additions & 0 deletions Tests/ReactorKitTests/StateRelayTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//
// StateRelayTests.swift
// ReactorKit
//
// Created by tokijh on 21/10/2018.
//

import XCTest
import RxExpect
import RxSwift
import RxTest
@testable import ReactorKit

class StateRelayTests: XCTestCase {
func testInitialValues() {
let a = StateRelay(value: 1)
let b = StateRelay(value: 2)

let c = Observable.combineLatest(a.asObservable(), b.asObservable(), resultSelector: +)

var latestValue: Int?

let subscription = c
.subscribe(onNext: { next in
latestValue = next
})

XCTAssertEqual(latestValue!, 3)

a.value = 5

XCTAssertEqual(latestValue!, 7)

b.value = 9

XCTAssertEqual(latestValue!, 14)

subscription.dispose()

a.value = 10

XCTAssertEqual(latestValue!, 14)
}

func testAccept() {
let relay = StateRelay(value: 0)

relay.accept(100)
XCTAssertEqual(relay.value, 100)

relay.accept(200)
XCTAssertEqual(relay.value, 200)

relay.accept(300)
XCTAssertEqual(relay.value, 300)
}

func testDoNotSendsCompletedOnDealloc() {
var a = StateRelay(value: 1)

var latest = 0
var completed = false
let disposable = a.asObservable().debug().subscribe(onNext: { n in
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh.. I committed .debug()..!

latest = n
}, onCompleted: {
completed = true
})

XCTAssertEqual(latest, 1)
XCTAssertFalse(completed)

a = StateRelay(value: 2)

XCTAssertEqual(latest, 1)
XCTAssertFalse(completed)

disposable.dispose()

XCTAssertEqual(latest, 1)
XCTAssertFalse(completed)
}

func testVariableREADMEExampleByStateRelay() {

// Two simple Rx variables
// Every variable is actually a sequence future values in disguise.
let a /*: Observable<Int>*/ = StateRelay(value: 1)
let b /*: Observable<Int>*/ = StateRelay(value: 2)

// Computed third variable (or sequence)
let c /*: Observable<Int>*/ = Observable.combineLatest(a.asObservable(), b.asObservable()) { $0 + $1 }

// Reading elements from c.
// This is just a demo example.
// Sequence elements are usually never enumerated like this.
// Sequences are usually combined using map/filter/combineLatest ...
//
// This will immediately print:
// Next value of c = 3
// because variables have initial values (starting element)
var latestValueOfC : Int? = nil
// let _ = doesn't retain.
let d/*: Disposable*/ = c
.subscribe(onNext: { c in
//print("Next value of c = \(c)")
latestValueOfC = c
})

defer {
d.dispose()
}

XCTAssertEqual(latestValueOfC!, 3)

// This will print:
// Next value of c = 5
a.value = 3

XCTAssertEqual(latestValueOfC!, 5)

// This will print:
// Next value of c = 8
b.value = 5

XCTAssertEqual(latestValueOfC!, 8)
}
}