svdo / ReRxSwift

ReRxSwift: RxSwift bindings for ReSwift
https://svdo.github.io/ReRxSwift
MIT License
99 stars 16 forks source link
frp functional-reactive-programming reactive-programming reswift rxswift swift4

ReRxSwift

Swift Version 4.1 Build Status API Documentation Carthage compatible CocoaPods Version Badge Supported Platforms Badge license

RxSwift bindings for ReSwift

Table of Contents

Introduction

In case you don't know them yet, these are two awesome frameworks:

Using those two frameworks you can make very nice app architectures. RxSwift allows you to bind application state to your UI, and ReSwift emits state updates in response to actions. We take it one step further though.

Similar to react-redux, ReRxSwift allows you to create view controllers that have props and actions. View controllers read all data they need from their props (instead of directly from the state), and they change data by invoking callbacks defined by actions (instead of directly dispatching ReSwift actions). This has some nice advantages:

Installation

The easiest way to use this library is through Cocoapods or Carthage. For CocoaPods, add this to your Podfile:

pod 'ReRxSwift', '~> 2.0'

For Carthage, add this to your Cartfile:

github "svdo/ReRxSwift" ~> 2.0

Usage

This section assumes that there is a global variable store that contains your app's store, and that it's type is Store<AppState>. You have a view controller that manages a text field; the text field displays a value from your state, and on editingDidEnd you trigger an action to store the text field's content back into your state. To use ReRxSwift for your view controller MyViewController, you use the following steps.

  1. Create an extension to your view controller to make it Connectable, defining the Props and Actions that your view controller needs:

    extension MyViewController: Connectable {
        struct Props {
            let text: String
        }
        struct Actions {
            let updatedText: (String) -> Void
        }
    }
  2. Define how your state is mapped to the above Props type:

    private let mapStateToProps = { (appState: AppState) in
        return MyViewController.Props(
            text: appState.content
        )
    }
  3. Define the actions that are dispatched:

    private let mapDispatchToActions = { (dispatch: @escaping DispatchFunction) in
        return MyViewController.Actions(
            updatedText: { newText in dispatch(SetContent(newContent: newText)) }
        )
    }
  4. Define the connection and hook it up:

    class MyViewController: UIViewController {
        @IBOutlet weak var textField: UITextField!
    
        let connection = Connection(
            store: store,
            mapStateToProps: mapStateToProps,
            mapDispatchToActions: mapDispatchToActions
        )
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            connection.connect()
        }
    
        override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(animated)
            connection.disconnect()
        }
    }
  5. Bind the text field's text, using a Swift 4 key path to refer to the text property of Props:

    override func viewDidLoad() {
        super.viewDidLoad()
        connection.bind(\Props.text, to: textField.rx.text)
    }
  6. Call the action:

    @IBAction func editingChanged(_ sender: UITextField) {
        actions.updatedText(sender.text ?? "")
    }

This is pretty much the SimpleTextFieldViewController inside the sample app. That view controller comes with complete unit tests: SimpleTextFieldViewControllerSpec.

API

Below is a high-level description of the most important components of the API. There is also full API documentation of ReRxSwift available.

Connectable

This is the protocol that your view controller has to conform to. It requires you to add a connection property. It provides the props and actions that you can use in your view controller. Normally, you declare the connection as follows:

class MyViewController: Connectable {
    let connection = Connection(
        store: store,
        mapStateToProps: mapStateToProps,
        mapDispatchToActions: mapDispatchToActions
    )
}

Refer to the Connection constructor documentation for more information.

props

This contains the Props object that you create using mapStateToProps. In other words: it contains all data that your view controller uses, automatically extracted from your application state. When using the bind methods in Connection, you probably don't need to use this props property directly.

actions

This contains the Actions object that you create using mapDispatchToActions. In other words: it specifies which ReSwift action has to be dispatched when calling the callbacks defined by your actions.

Connection

The Connection takes care of the mapping from you application state to your view controller props, and of dispatching the mapped action when calling functions in your view controller actions.

Constructor(store, mapStateToProps, mapDispatchToActions)

To create your Connection instance, you need to construct it with three parameters:

connect()

Calling this method causes the connection to subscribe to the ReSwift store and receive application state updates. Call this from your view controller's viewWillAppear or viewDidAppear method.

disconnect()

Calling this method causes the connection to unsubscribe from the ReSwift store. Call this from your view controller's viewWillDisappear or viewDidDisappear.

subscribe(keyPath, onNext)

Subscribe to an entry in your view controller's props, meaning that the given onNext handler will be called whenever that entry changes.

bind(keyPath, to, mapping)

This function binds an entry in your view controller's props to a RxSwift-enabled user interface element, so that every time your props change, the user interface element is updated accordingly, automatically.

The function bind takes the following parameters:

Just for your understanding: there are several variants of the bind function. They are all variants of this simplified code:

self.props
    .asObservable()
    .distinctUntilChanged { $0[keyPath: keyPath] == $1[keyPath: keyPath] }
    .map { $0[keyPath: keyPath] }
    .bind(to: observer)
    .disposed(by: disposeBag)

Example App

The folder Example contains the following examples:

FAQ

My props are not updated when the application state changes?

This happens when you forget to call connection.connect() in you view controller's viewWillAppear or viewDidAppear method. While you're at it, you may want to verify that you also call connection.disconnect() in viewWillDisappear or viewDidDisappear.

I get compiler errors when calling connection.bind()?

When calling bind, you pass a key path to an element in your props object. Because of the way ReRxSwift makes sure to only trigger when this element actually changed, it compares its value with the previous one. This means that the elements in your props object need to be Equatable. Simple types of course are already Equatable, but especially when binding table view items or collection view items, you need to make sure that the types are Equatable.

Another common source of errors is when the type of the element in your props does not exactly match the expected type by the user interface element. For example, you bind to a stepper's stepValue, which is a Double, but your props contains a Float. In these cases you can pass a mapping function as the third parameter to bind(_:to:mapping:) to cast the props element to the expected type. See SteppingUpViewController.swift for examples.

I double-checked everything and I still get errors!

Please open a new issue on GitHub, as you may have run into a bug. (But please make sure everything inside your Props type is Equatable!)