Coder Social home page Coder Social logo

reactorkit / reactorkit Goto Github PK

View Code? Open in Web Editor NEW
2.7K 68.0 267.0 634 KB

A library for reactive and unidirectional Swift applications

Home Page: http://reactorkit.io

License: MIT License

Swift 92.18% Ruby 1.99% Makefile 1.95% Objective-C 3.88%
swift reactorkit reactive unidirectional architecture rxswift

reactorkit's Introduction

ReactorKit

Swift CocoaPods Platform CI Codecov

ReactorKit is a framework for a reactive and unidirectional Swift application architecture. This repository introduces the basic concept of ReactorKit and describes how to build an application using ReactorKit.

You may want to see the Examples section first if you'd like to see the actual code. For an overview of ReactorKit's features and the reasoning behind its creation, you may also check the slides from this introductory presentation over at SlideShare.

Table of Contents

Basic Concept

ReactorKit is a combination of Flux and Reactive Programming. The user actions and the view states are delivered to each layer via observable streams. These streams are unidirectional: the view can only emit actions and the reactor can only emit states.

flow

Design Goal

  • Testability: The first purpose of ReactorKit is to separate the business logic from a view. This can make the code testable. A reactor doesn't have any dependency to a view. Just test reactors and test view bindings. See Testing section for details.
  • Start Small: ReactorKit doesn't require the whole application to follow a single architecture. ReactorKit can be adopted partially, for one or more specific views. You don't need to rewrite everything to use ReactorKit on your existing project.
  • Less Typing: ReactorKit focuses on avoiding complicated code for a simple thing. ReactorKit requires less code compared to other architectures. Start simple and scale up.

View

A View displays data. A view controller and a cell are treated as a view. The view binds user inputs to the action stream and binds the view states to each UI component. There's no business logic in a view layer. A view just defines how to map the action stream and the state stream.

To define a view, just have an existing class conform a protocol named View. Then your class will have a property named reactor automatically. This property is typically set outside of the view.

class ProfileViewController: UIViewController, View {
  var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // inject reactor

When the reactor property has changed, bind(reactor:) gets called. Implement this method to define the bindings of an action stream and a state stream.

func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}

Storyboard Support

Use StoryboardView protocol if you're using a storyboard to initialize view controllers. Everything is same but the only difference is that the StoryboardView performs a binding after the view is loaded.

let viewController = MyViewController()
viewController.reactor = MyViewReactor() // will not executes `bind(reactor:)` immediately

class MyViewController: UIViewController, StoryboardView {
  func bind(reactor: MyViewReactor) {
    // this is called after the view is loaded (viewDidLoad)
  }
}

Reactor

A Reactor is an UI-independent layer which manages the state of a view. The foremost role of a reactor is to separate control flow from a view. Every view has its corresponding reactor and delegates all logic to its reactor. A reactor has no dependency to a view, so it can be easily tested.

Conform to the Reactor protocol to define a reactor. This protocol requires three types to be defined: Action, Mutation and State. It also requires a property named initialState.

class ProfileViewReactor: Reactor {
  // represent user actions
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // represent state changes
  enum Mutation {
    case setFollowing(Bool)
  }

  // represents the current view state
  struct State {
    var isFollowing: Bool = false
  }

  let initialState: State = State()
}

An Action represents a user interaction and State represents a view state. Mutation is a bridge between Action and State. A reactor converts the action stream to the state stream in two steps: mutate() and reduce().

flow-reactor

mutate()

mutate() receives an Action and generates an Observable<Mutation>.

func mutate(action: Action) -> Observable<Mutation>

Every side effect, such as an async operation or API call, is performed in this method.

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive an action
    return UserAPI.isFollowing(userID) // create an API stream
      .map { (isFollowing: Bool) -> Mutation in
        return Mutation.setFollowing(isFollowing) // convert to Mutation stream
      }

  case let .follow(userID):
    return UserAPI.follow()
      .map { _ -> Mutation in
        return Mutation.setFollowing(true)
      }
  }
}

reduce()

reduce() generates a new State from a previous State and a Mutation.

func reduce(state: State, mutation: Mutation) -> State

This method is a pure function. It should just return a new State synchronously. Don't perform any side effects in this function.

func reduce(state: State, mutation: Mutation) -> State {
  var state = state // create a copy of the old state
  switch mutation {
  case let .setFollowing(isFollowing):
    state.isFollowing = isFollowing // manipulate the state, creating a new state
    return state // return the new state
  }
}

transform()

transform() transforms each stream. There are three transform() functions:

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>

Implement these methods to transform and combine with other observable streams. For example, transform(mutation:) is the best place for combining a global event stream to a mutation stream. See the Global States section for details.

These methods can be also used for debugging purposes:

func transform(action: Observable<Action>) -> Observable<Action> {
  return action.debug("action") // Use RxSwift's debug() operator
}

Advanced

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 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 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:

var currentUser: BehaviorSubject<User> // global state

func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
  return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}

Then the mutation will be emitted each time the view sends an action to a reactor and the currentUser is changed.

View Communication

You must be familiar with callback closures or delegate patterns for communicating between multiple views. ReactorKit recommends you to use reactive extensions for it. The most common example of ControlEvent is UIButton.rx.tap. The key concept is to treat your custom views as UIButton or UILabel.

view-view

Let's assume that we have a ChatViewController which displays messages. The ChatViewController owns a MessageInputView. When an user taps the send button on the MessageInputView, the text will be sent to the ChatViewController and ChatViewController will bind in to the reactor's action. This is an example MessageInputView's reactive extension:

extension Reactive where Base: MessageInputView {
  var sendButtonTap: ControlEvent<String> {
    let source = base.sendButton.rx.tap.withLatestFrom(...)
    return ControlEvent(events: source)
  }
}

You can use that extension in the ChatViewController. For example:

messageInputView.rx.sendButtonTap
  .map(Reactor.Action.send)
  .bind(to: reactor.action)

Testing

ReactorKit has a built-in functionality for a testing. You'll be able to easily test both a view and a reactor with a following instruction.

What to test

First of all, you have to decide what to test. There are two things to test: a view and a reactor.

  • View
    • Action: is a proper action sent to a reactor with a given user interaction?
    • State: is a view property set properly with a following state?
  • Reactor
    • State: is a state changed properly with an action?

View testing

A view can be tested with a stub reactor. A reactor has a property stub which can log actions and force change states. If a reactor's stub is enabled, both mutate() and reduce() are not executed. A stub has these properties:

var state: StateRelay<Reactor.State> { get }
var action: ActionSubject<Reactor.Action> { get }
var actions: [Reactor.Action] { get } // recorded actions

Here are some example test cases:

func testAction_refresh() {
  // 1. prepare a stub reactor
  let reactor = MyReactor()
  reactor.isStubEnabled = true

  // 2. prepare a view with a stub reactor
  let view = MyView()
  view.reactor = reactor

  // 3. send an user interaction programmatically
  view.refreshControl.sendActions(for: .valueChanged)

  // 4. assert actions
  XCTAssertEqual(reactor.stub.actions.last, .refresh)
}

func testState_isLoading() {
  // 1. prepare a stub reactor
  let reactor = MyReactor()
  reactor.isStubEnabled = true

  // 2. prepare a view with a stub reactor
  let view = MyView()
  view.reactor = reactor

  // 3. set a stub state
  reactor.stub.state.value = MyReactor.State(isLoading: true)

  // 4. assert view properties
  XCTAssertEqual(view.activityIndicator.isAnimating, true)
}

Reactor testing

A reactor can be tested independently.

func testIsBookmarked() {
  let reactor = MyReactor()
  reactor.action.onNext(.toggleBookmarked)
  XCTAssertEqual(reactor.currentState.isBookmarked, true)
  reactor.action.onNext(.toggleBookmarked)
  XCTAssertEqual(reactor.currentState.isBookmarked, false)
}

Sometimes a state is changed more than one time for a single action. For example, a .refresh action sets state.isLoading to true at first and sets to false after the refreshing. In this case it's difficult to test state.isLoading with currentState so you might need to use RxTest or RxExpect. Here is an example test case using RxSwift:

func testIsLoading() {
  // given
  let scheduler = TestScheduler(initialClock: 0)
  let reactor = MyReactor()
  let disposeBag = DisposeBag()

  // when
  scheduler
    .createHotObservable([
      .next(100, .refresh) // send .refresh at 100 scheduler time
    ])
    .subscribe(reactor.action)
    .disposed(by: disposeBag)

  // then
  let response = scheduler.start(created: 0, subscribed: 0, disposed: 1000) {
    reactor.state.map(\.isLoading)
  }
  XCTAssertEqual(response.events.map(\.value.element), [
    false, // initial state
    true,  // just after .refresh
    false  // after refreshing
  ])
}

Pulse

Pulse has diff only when mutated To explain in code, the results are as follows.

var messagePulse: Pulse<String?> = Pulse(wrappedValue: "Hello tokijh")

let oldMessagePulse: Pulse<String?> = messagePulse
messagePulse.value = "Hello tokijh" // add valueUpdatedCount +1

oldMessagePulse.valueUpdatedCount != messagePulse.valueUpdatedCount // true
oldMessagePulse.value == messagePulse.value // true

Use when you want to receive an event only if the new value is assigned, even if it is the same value. like alertMessage (See follows or PulseTests.swift)

// Reactor
private final class MyReactor: Reactor {
  struct State {
    @Pulse var alertMessage: String?
  }

  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case let .alert(message):
      return Observable.just(Mutation.setAlertMessage(message))
    }
  }

  func reduce(state: State, mutation: Mutation) -> State {
    var newState = state

    switch mutation {
    case let .setAlertMessage(alertMessage):
      newState.alertMessage = alertMessage
    }

    return newState
  }
}

// View
reactor.pulse(\.$alertMessage)
  .compactMap { $0 } // filter nil
  .subscribe(onNext: { [weak self] (message: String) in
    self?.showAlert(message)
  })
  .disposed(by: disposeBag)

// Cases
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.doSomeAction)    // showAlert() is not called
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.alert("tokijh")) // showAlert() is called with `tokijh`
reactor.action.onNext(.doSomeAction)    // showAlert() is not called

Examples

  • Counter: The most simple and basic example of ReactorKit
  • GitHub Search: A simple application which provides a GitHub repository search
  • RxTodo: iOS Todo Application using ReactorKit
  • Cleverbot: iOS Messaging Application using Cleverbot and ReactorKit
  • Drrrible: Dribbble for iOS using ReactorKit (App Store)
  • Passcode: Passcode for iOS RxSwift, ReactorKit and IGListKit example
  • Flickr Search: A simple application which provides a Flickr Photo search with RxSwift and ReactorKit
  • ReactorKitExample
  • reactorkit-keyboard-example: iOS Application example for develop keyboard-extensions using ReactorKit Architecture.
  • SWHub: Use ReactorKit develop the Github client

Dependencies

Requirements

  • Swift 5
  • iOS 8
  • macOS 10.11
  • tvOS 9.0
  • watchOS 2.0

Installation

Podfile

pod 'ReactorKit'

Package.swift

let package = Package(
  name: "MyPackage",
  dependencies: [
    .package(url: "https://github.com/ReactorKit/ReactorKit.git", .upToNextMajor(from: "3.0.0"))
  ],
  targets: [
    .target(name: "MyTarget", dependencies: ["ReactorKit"])
  ]
)

ReactorKit does not officially support Carthage.

Cartfile

github "ReactorKit/ReactorKit"

Most Carthage installation issues can be resolved with the following:

carthage update 2>/dev/null
(cd Carthage/Checkouts/ReactorKit && swift package generate-xcodeproj)
carthage build

Contribution

Any discussions and pull requests are welcomed 💖

  • To development:

    $ TEST=1 swift package generate-xcodeproj
  • To test:

    $ swift test

Community

Join

Community Projects

Who's using ReactorKit


StyleShare Kakao Wantedly

DocTalk Constant Contact KT

Hyperconnect Toss LINE Pay

LINE Pay Kurly

Are you using ReactorKit? Please let me know!

Changelog

  • 2017-04-18
    • Change the repository name to ReactorKit.
  • 2017-03-17
    • Change the architecture name from RxMVVM to The Reactive Architecture.
    • Every ViewModels are renamed to ViewReactors.

License

ReactorKit is under MIT license. See the LICENSE for more info.

reactorkit's People

Contributors

changm4n avatar creasty avatar dependabot[bot] avatar devxoul avatar dodgecm avatar doulos76 avatar finestructure avatar gettoset avatar gre4ixin avatar jihoonahn avatar jsryudev avatar kasimte avatar kemchenj avatar kickbell avatar kimdarren avatar kubode avatar mariohahn avatar minchaozhang avatar murselturk avatar ohkanghoon avatar sanghun0724 avatar seivan avatar seonghun23 avatar taejoongyoon avatar techinpark avatar tokijh avatar wanbok avatar woin2ee avatar wplong11 avatar yhkaplan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

reactorkit's Issues

Can't transform the global state to a mutation while observing this state

Hi, I want to transform the global state to a mutation. It is perfect to use

func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
    return .merge(mutation, anotherMutationFromGlobalState())
}

But I need to use reactor's state to determinate if I really need this mutation from global state. So my code looks like

  func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
    return .merge(mutation, positionMutation(mutation))
  }

  private func positionMutation(_ mutation: Observable<Mutation>) -> Observable<Mutation> {
    return mutation
      .flatMapLatest { [weak self] mutation -> Observable<CLLocation> in
        guard let `self` = self, case .autoTrackingSwitched(true) = mutation else { return .empty() }
        return self.trackingService.track()
      }
      .map(Mutation.updatePosition)
  }

But .merge(mutation, positionMutation(mutation)) breaks action-mutation-state flow, because it emits double mutation instead of one, so all my actions duplicated. Сan I transform mutation in Reactor with observing Reactor state(or mutation)? For better understanding my business logic: I have the UISwitch in UIViewController and I want to get locations from service with protocol func track() -> Observable<CLLocation> when UISwitch is On and want to dispose this observable when the UISwitch is Off.

Build Xcode9 with Carthage

Hi there~

I'm using ReactorKit from Carthage.

When build with Xcode9, there are error because ReactorKit compiled with 3.1

So, I try to rebuild framework using carthage update --no-use-binaries then,
terminal say Dependency "ReactorKit" has no shared framework schemes

Carthage: no shared scheme

Hi,

currently it is not possible to install ReactorKit using Carthage.

*** Skipped building ReactorKit due to the error:
Dependency "ReactorKit" has no shared framework schemes for any of the platforms: iOS

Repeated action every x second

Good day guys,

Trying to figure out how could I achieve an action every 20s (like you would by using a NSTimer.scheduledTimerWithTimeInterval call).

Essentially on startup I would like to make startWith the Action.fetchSomething and schedule that action every 20s.

I have troubles figuring out how I could achieve this.

Thank you in advance

Complex observable

Hi,

how would you solve problem when with one action you want to create a observable and with another action you want to do something with this already created observable?

For example, I have a observable which will emit Mutation after 10 seconds:

switch action {
    case .start:
        return Observable<Int>.interval(10, scheduler: MainScheduler.instance)
            .map { _ in .doSomething }

and now I wanna dispose or restart this observable.

switch action {
    case .stop:
        // What to do now?

How would you do that?

Automated tasks & transitioning UI between states

Lets imagine you have some application that requires synchronisation. This synchronisation is ran every time a user opens the app (becomes active). How does this fit to ReactorKit, where this should be implemented?

When sync completes, it usually changes UI in terms of adding/removing items from table view etc. so eventually it has to somehow update the State of one or more Reactors.

This brings me to another thing with transitioning between states - like add rows to UITable/CollectionView, animating changes etc. How this should be done using ReactorKt if there is some best practice for this or a guideline.

Reactor inheritance

Is there any way to inherit a Reactor implementation so that we can have a subclass with all the actions, states and mutations from the parent, but also add new functionality?

Triggering Action

Is there any chance to trigger the Action without subscribing to State?

For Example:
I have Action saveKey(Key)

enum Action {
  case saveKey(Key)
}

and I want to trigger action case without returning any Mutation case.

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .saveKey(key):
    userService.appendKey(key)
    return .empty()
  }
}

Should I return .empty() observable? But it doesn't work

Is there any ways to prevent the fat bind function?

I have a question.
When using Reactorkit, I sometimes have the fat bind function which has more than 100 lines.

Even I do not know whether it is good or bad, but if you have any idea to prevent this, please let me know.

Showing errors and updating input fields

Is there any best practice, recommendation, a guideline on how to display error that occurred in Mutation Observable?

Eg 1: I am fetching data from web service and I get my auth token expired error so I need to somehow present this in UI.

Eg 2: I have some UITextField. On some action, that gets called I will want the mutation to update the text in this field. How is this achieved?

Thanks!

installing via Carthage not working

carthage update ReactorKit platform iOS
result:
*** Skipped building ReactorKit due to the error: Dependency "ReactorKit" has no shared framework schemes

Combined reducers?

Should introduce a way to make a global application store that can combine multiple reducers.
This helps if you want to dispatch actions that are captured in a middleware for logging purposes and have application wide subscriptions on the store.

As of now, I see this for being a per viewmodel basis, but there is nothing preventing one to use a store for the entire application as well to keep content and state.

Do you see any way to get a Redux like behaviour here?

RxGesture's typealias 'View' name conflicted

I found naming issue in using ReactorKit with RxGesture

In RxGesture define View also ReactorKit too

// Copyright (c) RxSwiftCommunity

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import Foundation

#if os(iOS)
    import UIKit
    public typealias GestureRecognizer = UIGestureRecognizer
    public typealias GestureRecognizerState = UIGestureRecognizerState
    public typealias GestureRecognizerDelegate = UIGestureRecognizerDelegate
    public typealias View = UIView
#elseif os(OSX)
    import AppKit
    public typealias GestureRecognizer = NSGestureRecognizer
    public typealias GestureRecognizerState = NSGestureRecognizerState
    public typealias GestureRecognizerDelegate = NSGestureRecognizerDelegate
    public typealias View = NSView
#endif

In screenshot

2017-08-02 11 31 13

how can i use both library?

Carthage build error

Hi,

When compiling with Carthage, I get the following error:

image

Could you please help me?

Thanks

collectionView LoadMore issue

Hi, I tried to implement the example loadmore function with a collectionView (instead of tableview) (Just change everything to collectionview) There is an issue with collectionview

Step to reproduce

  • User search and scrolling down to activate the loadmore

  • User type another search

The screen gone blank (data is still fetch successfully) (can see all the response).
Is there any example that loadmore function work with collectionView (or this is a bug?)

a question

when my controller has a collectionview or tableview, how to resolve the complexity in reshresh/endRefresh/loadMore/endLoadMore

Proposal - Improvement of disposeBag of View Protocol

Hi,

The disposeBag of View protocol is released for each set of Reactor (View.siwftL40).
This behavior is likely to cause confusion if disposeBag is used outside of bind(reactor:).

I thought some improvement ideas.
I'd really appreciate it if you could give me your opinion.

Implementation 1

Make disposeBag as associatedObject like Reactor protocol, and also return disposeBag with bind (reactor:).

public protocol View: class, AssociatedObjectStore {
  associatedtype Reactor: _Reactor

  var disposeBag: DisposeBag { get set }
  var reactor: Reactor? { get set }

  /// Binds View with Reactor.
  ///
  /// - warning: Don't call this method directly.
  func bind(reactor: Reactor, disposeBag: DisposeBag)
}


// MARK: - Associated Object Keys

private var disposeBagKey = "disposeBag"
private var reactorKey = "reactor"

// MARK: - Default Implementations

extension View {
  public var disposeBag: DisposeBag {
    get { return self.associatedObject(forKey: &disposeBagKey, default: DisposeBag()) }
    set { self.setAssociatedObject(newValue, forKey: &disposeBagKey) }
  }
  
  public var reactor: Reactor? {
    get { return self.associatedObject(forKey: &reactorKey) }
    set {
      self.setAssociatedObject(newValue, forKey: &reactorKey)
      self.disposeBag = DisposeBag()
      if let reactor = newValue {
        self.bind(reactor: reactor, disposeBag: self.disposeBag)
      }
    }
  }
}

Implementation 2

Make internal disposeBag, and also return internal disposeBag with bind (reactor:).

public typealias _View = View
public protocol View: class, AssociatedObjectStore {
  associatedtype Reactor: _Reactor

  var reactor: Reactor? { get set }

  /// Binds View with Reactor.
  ///
  /// - warning: Don't call this method directly.
  func bind(reactor: Reactor, disposeBag: DisposeBag)
}


// MARK: - Associated Object Keys

private var disposeBagKey = "disposeBag"
private var reactorKey = "reactor"

// MARK: - Default Implementations

extension View {
  private var internalDisposeBag: DisposeBag {
    get { return self.associatedObject(forKey: &disposeBagKey, default: DisposeBag()) }
    set { self.setAssociatedObject(newValue, forKey: &disposeBagKey) }
  }
  
  public var reactor: Reactor? {
    get { return self.associatedObject(forKey: &reactorKey) }
    set {
      self.setAssociatedObject(newValue, forKey: &reactorKey)
      self.internalDisposeBag = DisposeBag()
      if let reactor = newValue {
        self.bind(reactor: reactor, disposeBag: self.internalDisposeBag)
      }
    }
  }
}

Implementation 3

Add comments about release timing to disposeBag.

reactor.state subscribe onNext twice????

func bind(reactor: HomeViewReactor) {
        // Action
        self.rx.viewDidLoad
            .map{ Reactor.Action.initializationTest }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        // State
        
        reactor.state
            .map { $0.test }
            .debug()
            .filterNil()
            .subscribe(onNext: { (test) in
                print("---------call it back twice-----------")
                print(test)
            })
            .disposed(by: disposeBag)
}

I feel like crying when I meet this question。。。

Why differentiate between actions and mutations?

I understand that mutations are the effect of side effects, but aren't they essentially the same thing in the examples?

It reminds me a little bit of https://redux-observable.js.org/docs/basics/Epics.html (which is nice!) but they don't differentiate between actions in epics (what you call mutations).
Instead they wrap their async actions in an observable

It could just be three actions:

enum Action {
    case refreshFollowingStatus(Int) //async
    case follow(Int) //async
    case followed(Bool)
  }

ReactorKit with nib files

All examples do not use neither storyboard nor nib files. How should I resolve situation when I want use nib files for viewcontrollers? When I do it now, app crashes because IBOutlets are not initialized yet and I need access them in bind method.

I want to do something like this, but self.collectionView is nil:

reactor.state
            .asObservable()
            .map { [CategorySectionModel(model: Void(), items: $0.categories)] }
            .bind(to: self.collectionView.rx.items(dataSource: self.dataSource))
            .addDisposableTo(self.disposeBag)

How to tell difference between 2 actions that result in same mutation

Hello,

I am building an application that is using UITableView for displaying list of some items. There is also some other screen where you can pick a filter for those items. The problem I encountered with state is that I have an array of those items in state and I am running diff on that array whenever state changes so I can remove/insert rows from/to table view with animation. However, when I change the filter, I want to actually do this without the animation. The issue is that I do not, know how to tell when it is just items that changed for whatever reason and when the items changed because of the new filter was applied. Even if I have filter in my state struct.

Maybe my whole approach is wrong.

I have action .subscribeToListChanges that generates a mutation whenever something in Realm changes (it is basically never completing Observable). Then I have action .setFilter that sets filter. That changes filter which triggers also new mutation from .subscribeToListChanges Observable.

Perform initial action

Hey, I'd like to perform initial action when controller is loaded. How do I do?

There's an example of things I'd do in Cocoa world

Snippet
// Let's say we're in viewDidLoad()

let store = HKHealthStore()

guard HKHealthStore.isHealthDataAvailable() else {
    unsupportedDevicePopup.show() // ex
    return
}

guard let stepsType = HKObjectType.quantityType(forIdentifier: .stepCount) else {
    fatalError()
}

store.requestAuthorization(toShare: nil, read: [stepsType]) { success, err in
    guard success else {
        if let err = err {
            infoPopup.text = err.localizedDescription
            infoPopup.show() // ex
        }
        return
    }
    self.startProcessingData()
    self.showNextScreen()
}
How do I apply this to an Rx / ReactorKit's paradigm?

Git submodules support

Hi,

There is no xcodeproj file for this project, so I can't use it as a submodule. Can you please add support for that.

Thanks!

Async operation before reduce

Hi! I was wondering why you perform async operations before the reduce. What if the async operation requires some variable stored in your previous state. Isn't the previous state only accessible in reduce? How would I go about handling this case?

State bind multiple times

Hi, I download the example of Drrrible, and now I have a question, such as ShotListViewController

// Output
reactor.state.map { $0.isRefreshing }
  .distinctUntilChanged()
  .bind(to: self.refreshControl.rx.isRefreshing)
  .disposed(by: self.disposeBag)

reactor.state.map { $0.sections }
  .bind(to: self.collectionView.rx.items(dataSource: self.dataSource))
  .disposed(by: self.disposeBag)

I disable the response stream of sections, but the signal of sections in state be triggered multiple times:

    case .refresh:
      guard !self.currentState.isRefreshing else { return .empty() }
      guard !self.currentState.isLoading else { return .empty() }
      let startRefreshing = Observable<Mutation>.just(.setRefreshing(true))
      let endRefreshing = Observable<Mutation>.just(.setRefreshing(false))
//      let setShots = self.shotService.shots(paging: .refresh)
//        .map { list -> Mutation in
//          return .setShots(list.items, nextURL: list.nextURL)
//        }
      return .concat([startRefreshing, endRefreshing])

In my project, I want to just trigger one state,could you help me?

How to call side effect on page load

Hi I am using StoryBoardView . How can I call a webservice on page load .
say

enum Action {
    case loadLocalData
    case loadDataFromServer
}


func mutate(action: Action) -> Observable<Mutation>{
    switch action {
    case .loadLocalData:
        return .concat([
            Observable.just(Mutation.setLoading(true)),
            
            self.testService.fetchLocal().map{
                
                Mutation.setPropertyData($0)
            },
            
            Observable.just(Mutation.setLoading(false)),
        ])
    case .loadDataFromServer:
        return .concat([
            Observable.just(Mutation.setLoading(true)),
            
            
            self.testService.fetchFromServer().map{
                Mutation.setPropertyData($0)
            },
            
            Observable.just(Mutation.setLoading(false)),
        ])
    }
}

But how to call set load from Server at first place.
P>S New to reactorkit

Service layer?

Hi,

The documentation states that ReactorKit has a special Service layer to do every heavy or asynchronous operation, but I cannot find the Service protocol/class in the source code.

Is it something that was removed from code but not from the documentation?

Thanks,
M

UIcollectionView select and deselect issue

Hi i'm facing a problem with collectionview select and deselct (cannot deselect after selected)

My setup is

PhotoListViewReactor.class

class PhotoListViewReactor : Reactor {
        
        enum Action {
            case shareInit
            case select(photo: Photo)
            case deselect(photo: Photo)
            case shareConfirm
            case shareFinish
        }
        
        enum Mutation {
            case selectShare(_ photo: Photo)
            case deselectShare(_ photo: Photo)
            case setSharingState(Bool)
            case triggerShareAction
            case shareComplete
        }
        
        struct State {
            var sharePhotos: [Photo] = []
            var isSharing: Bool = false
            var shareAction: Bool = false
        }
        
        var initialState = State()
        
        //    init() { }
        
        func mutate(action: Action) -> Observable<Mutation> {
            switch action {
                
            case .select(photo: let photo):
                return Observable.just(Mutation.selectShare(photo)).takeUntil(self.action.filter(isSharingAction))
                
            case .deselect(photo: let photo):
                return Observable.just(Mutation.deselectShare(photo)).takeUntil(self.action.filter(isSharingAction))
                
            case .shareInit:
                return Observable.just(Mutation.setSharingState(true))
                
            case .shareConfirm:
                return Observable.concat([Observable.just(Mutation.triggerShareAction), Observable.just(Mutation.setSharingState(false))])
            case .shareFinish:
                return Observable.concat([Observable.just(Mutation.shareComplete),Observable.just(Mutation.setSharingState(false))])
            }
        }
        
        func reduce(state: State, mutation: Mutation) -> State {
            switch mutation {
                
            case let .selectShare(photo):
                var newState = state
                newState.sharePhotos.append(photo)
                return newState
                
            case let .deselectShare(photo):
                var newState = state
                newState.sharePhotos.removeAll(where: { $0.id == photo.id })
                return newState
                
            case let .setSharingState(isSharing):
                var newState = state
                newState.isSharing = isSharing
                return newState
                
            case .triggerShareAction:
                var newState = state
                newState.shareAction = true
                return newState
                
            case .shareComplete:
                var newState = state
                newState.shareAction = false
                newState.isSharing = false
                newState.sharePhotos = []
                return newState
                
            }
        }
        
        private func isSharingAction(_ action: Action) -> Bool {
            if case .shareInit = action {
                return true
            } else {
                return false
            }
        }
        
    }

and inside PhotoListViewController

self.collectionView.rx.modelSelected(Photo.self).share()
            .filter(if: reactor.state.map{$0.isSharing})
            .map {Reactor.Action.select(photo: $0)}
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        
        self.collectionView.rx.modelDeselected(Photo.self).share()
            .filter(if: reactor.state.map{$0.isSharing})
            .map {Reactor.Action.deselect(photo: $0)}
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

To be clear my filterIf Operator:

extension ObservableType {
        
        /**
         Filters the source observable sequence using a trigger observable sequence producing Bool values.
         Elements only go through the filter when the trigger has not completed and its last element was true. If either source or trigger error's, then the source errors.
         - parameter trigger: Triggering event sequence.
         - returns: Filtered observable sequence.
         */
        func filter(if trigger: Observable<Bool>) -> Observable<E> {
            return self.withLatestFrom(trigger) { ($0, $1) }
                .filter { $0.1 }
                .map { $0.0 }
        }
    }

My problem is the selection and deselection does not work properly (once select, user can not deselect the cell by click again). I have enable multisection in uicollectionview. is there an issue with selection?

Carthage support?

There is no xcodeproj file for this project, so I can't use it by carthage. Can you please add support for that.

Thanks!

Mutation base on async call result

Hi ,
I found myself confused in handling muations in async call

 // action
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
            case let .requestSMSCode(phone):
                return createRequestSMSCodeTask(phone)
           ....
}

func createRequestSMSCodeTask(_ phone: String) -> Observable<Mutation> {
    
        let startLoading = Observable<Mutation>.just(
            .updateNetworkStatus(.loading("sending code"))
        )

        let createErrorLoading: (_ error: Error) ->Observable<Mutation> = { error in
            return Observable<Mutation>.just(
                    .updateNetworkStatus(.failed(" sent code failed"))
            )
        }

        let startCoundown = Observable<Int>.interval(1, scheduler: MainScheduler.instance).take(60).map(Mutation.updateCountDownLeftSeconds)

        let requestCode = self.userService.requestSMSCode(phone).asObservable()
            .map {_ in Mutation.updateNetworkStatus(.loaded("code sent")) }
            .catchError(createErrorLoading)
        
        return .concat([startLoading, requestCode, startCoundown ])
    }

whether to performstartCoundown task is based on requestCode task is success or failed,
but startCoundown task execution has no context to know pervious status

GET/POST asynchronous

I know that mutate is used for updating the state whenever actions happen, I am trying to make an API get request to receive an access token to make after that GET request, a POST request using that token in my authorization header in that post request, how do I make only one get request then the post request using that access token obtained from my first get request?

bind op only at `bind(reactor: RootViewReactor)`?

case :

 lazy var dataSource = RxCollectionViewSectionedReloadDataSource<BookListSection>(configureCell: {[unowned self] (_, collectionView, indexPath, element) in
        let cell: SearchCollectionViewCell = collectionView.dequeueReusableCell(for: indexPath)
        let model = element.initialState
        cell.isNeedShowOptButton = true
        cell.optButton.rx.tap
            .map { Reactor.Action.opt(indexPath) }
            .bind(to: self.reactor!.action)
            .disposed(by: self.disposeBag)

        cell.book = model
        cell.optButton.isHidden = false
        return cell
    }, configureSupplementaryView: {_, _, _, _ in (UICollectionReusableView()) })

this example can? not in bind method, have some bad influence?

ReactorKit Ignore initial action..

Hi, I'm developing a tableView with UISegmentedControl using ReactorKit .. and I found some problems in ReactorKit.

As far as I understand, the Reactor does not trigger mutate(), reduce() until state stream subscribed. That's why we have to do binding state explicitly to trigger action.(#41)
It's not a big problem in many cases, but when I use UIViews which have initial state (such as UISegmentedControl), I face some problem.
As mutate() does not being called until state subscribed (or binded), If we bind actions before state subscribed (as docs example does) it can disregard initial action of UIView.

So below snippet does not work as I intended, the second one worked perfectly for me.

First Version

// Action   
segmentedControl.rx.value
            .map(Reactor.Action.setSortType)
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

// State
 reactor.state
            .asObservable()
            .map { $0.comments }
            .map { [SectionModel(model: "comments", items: $0)] }
            .bind(to: tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)

Second Version

// State
 reactor.state
            .asObservable()
            .map { $0.comments }
            .map { [SectionModel(model: "comments", items: $0)] }
            .bind(to: tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)

// Action   
segmentedControl.rx.value
            .map(Reactor.Action.setSortType)
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

So I think it's safe to restrict bind ordering (State binding -> Action binding)
Or subscribe state explicitly when declear state stream

Consider making reducer run before mutations.

Given

State

struct State {
  var isLoading = false
  var tasks = [Task] 
}

Actions

enum Action {
  case requestTasks
  case requestTasksCompleted([Task])
}

Dispatching .requestTasks action first runs the reducer and sets isLoading = true thus the UI can update, e.g show a spinner.
Then in the mutation (what Redux-Observable[0] calls an "epic") you can start the request and on completion dispatch .requestTasksCompleted([Task]) for the reduce tor reload the list.

As of now, you would need three actions, e.g .setLoading , where before every requestTasks manually first dispatch .setLoading.

Edit: Could even use generics to ensure that the actions for Input is not the same as Output in the mutation so you don't get a infinite loop which they can't do with JS :-)

0

Documentation README link is incorrect.

When I cliecked documentation's toturial link, I got 404 error.

because of this.

https://github.com/ReactorKit/ReactorKit/blob/master/Documentation/Documentation/Tutorials/GitHubSearch/1-BuildingUserInterface.md

It is wrote Documentation path twice.

viewDidLoad called after bind

I have a view controller which I have instantiated using storyboard, my view controller conforms to protocol StoryboardView but my view did load is getting called after bind(reactor: ) method. Any reason what can cause this?

P.S. - I have a tab bar application and above controller is one of the tab bar item.

The state in transform(state: Observable<State>) is different than the current state

I am unsure that this is an issue or just me not understanding how state transformation works.

When using transform(state: Observable<State>) -> Observable<State>, previous transformations are not available in the parameter state.

For instance:

struct State {
  var flag = false
  var counter = 0
}

func transform(state: Observable<State>) -> Observable<State> {
  return state.map { state in
    var state = state
    state.counter += 1
    return state
  }
}

If the variable counter is not modified elsewhere (by a mutation for instance), it will always be equal to 1.

In the view:

function bind(reactor: MyReactor) {
  // Counter should count the number of state changes
  reactor.state.map { $0.counter }
    .bind {
      print($0) // Always prints 1
    }
    .disposed(by: self.disposeBag)

  // Perform some actions which mutate state.flag
  reactor.action.onNext(.setFlag(true))
  reactor.action.onNext(.setFlag(false))
  reactor.action.onNext(.setFlag(true))
  reactor.action.onNext(.setFlag(false))
}

I couldn't find any documentation regarding this point, so this might be how it is supposed to behave but I am unsure.

StoryboardView 프로토콜을 사용하였을 때, rx.viewDidLoad 이벤트를 받을 수 없습니다.

안녕하세요.

StoryboardView 프로토콜을 사용하였을 때, bind(reactor:) 함수에서
rx.viewDidLoad 이벤트를 받을 수 없습니다.

확인한 바로는 ReactorKitRuntime에서 viewDidLoad를 swizzle 하여
viewDidLoad가 호출 된 이후에 bind(reactor:)가 호출되기 때문인 것으로 확인됩니다.

viewDidLoad가 아닌 loadView 함수와 swizzle 하면 수정이 될 것으로 판단됩니다.

확인 부탁드립니다.

lazy property have Implement several times

我发现你定义属性的时候,是有用 then 定义的

let tableView = UITableView().then {
    $0.allowsSelectionDuringEditing = true
    $0.register(Reusable.taskCell)
  }

且为 let 常量,但是我这边有在嵌入这个库的时候,属性

lazy var tableView = {
    let tableview = UITableView()
    tableview.allowsSelectionDuringEditing = true 
    tableview.register(Reusable.taskCell)
}

在 bind 方法里面,我有

...
tableview.rx.setDelegate(self).disposedBy(disposebag)
...

然后在 viewDidLoad 中添加这个 tableview

self.view.addSubview(tableView)

运行程序,发现 lazy 的实现方法调用多次, 如果使用这个框架,UI 属性只能是 let 常量么?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.