Coder Social home page Coder Social logo

svdo / rerxswift Goto Github PK

View Code? Open in Web Editor NEW
99.0 13.0 16.0 2.24 MB

ReRxSwift: RxSwift bindings for ReSwift

Home Page: https://svdo.github.io/ReRxSwift

License: MIT License

Ruby 2.17% Objective-C 0.65% Swift 97.18%
reswift rxswift reactive-programming frp functional-reactive-programming swift4

rerxswift's Introduction

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:

  • Better separation of concerns. It is easier to understand what your view controller does and what data it uses. In other words, it facilitates local reasoning.
  • Unit-testing. Because of the separation of concerns, you can easily unit-test your view controllers, all the way up to the Interface Builder connections.
  • Better reusability. Reusing your view controllers is as simple as specifying different mappings from state to props and from ReSwift actions to actions. (See also section 'Open Issues' below.)
  • Rapid prototyping. You can easily use dummy props and actions so that you get a working UI layer prototype. Without writing any of your application's business logic, you can implement your presentation layer in such a way that it is very simple to replace the dummies with real state and actions.

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:

  • store: Your application's ReSwift store.
  • mapStateToProps: A function that takes values from your application's state and puts them in the view controller's props object. This decouples your application state from the view controller data.
  • mapDispatchToActions: A function that specifies which actions your view controller can call, and for each of those which ReSwift action needs to be dispatched.

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:

  • keyPath: The (Swift 4) key path that points to the element in your props that you want to bind to the user interface element.
  • to: The RxSwift reactive property wrapper, e.g. textField.rx.text or progressView.rx.progress.
  • mapping: Most of the bind variants (but not all of them) allow you to provide a mapping function. This mapping function is applied to the props element at the specified keyPath. You can use this for example for type conversions: your props contains a value as a Float, but the UI element requires it to be a String. Specifying the mapping { String($0) } will take care of that. SteppingUpViewController.swift contains an example of a mapping function that maps a Float value to the selected index of a segmented control.

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:

  • SimpleTextField: Most basic use case of ReRxSwift, containing a text field that has its value bound, and an action.
  • SteppingUp: Use case with multiple bound values and actions, also showing how to transform values when binding them.
  • TableAndCollection: Shows how to use ReRxSwift in combination with RxSwift, RxCocoa and RxDataSources to have very simple table/collection views that automatically animate their updates.

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!)

rerxswift's People

Contributors

0zguner avatar lucianopolit avatar svdo 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

Watchers

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

rerxswift's Issues

Are Optional props supported?

Hi there,

I'm used to working with the connect function from react-redux so it's really cool to see it implemented in swift.

I am trying to bind optional properties and the code won't compile.

let forecastKeyPath = \Props.forecast
connection.subscribe(forecastKeyPath) { [weak self] forecast in
    guard let forecast = forecast,
        let currently = forecast.currently else {
            return
    }

    self?.labelTemperature.text = currently.temperature
}

The error I get looks like:

Cannot convert value of type 'KeyPath<ViewController.Props, Forecast?>' to expected argument type 'KeyPath<_, [_]>'

This is my VC:

extension ViewController: Connectable {
    struct Props {
        let forecast: Forecast?
        let coordinate: CLLocationCoordinate2D
    }
    struct Actions {
    }
}

And I also made sure Forecast is Equatable

extension Forecast: Equatable {
    static func ==(lhs: Forecast, rhs: Forecast) -> Bool {
        if lhs.currently == nil || rhs.currently == nil {
            return false
        }
        return lhs.currently!.time == rhs.currently!.time
    }
}

Any advice would be greatly appreciated.

Cheers,

Handling async requests as a side effect?

Just looking at the docs, I wondered did you have any opinion on the best way to handle async calls, for example triggering a network request as a side effect of an action?

I'm thinking for example if I dispatch an action such as FetchBookFromAPI(id: "some-book-id") I would like that action to trigger a network request, by passing id as an argument to my networking client. On success of that request I can dispatch another action with the response as a payload and populate my store.

Looking at the ActionLogger I see how I can intercept requests, but I am struggling to see how, rather than creating a middleware for each async action, how I can trigger the request?

public func ActionLogger<S>() -> Middleware<S> {
    return { dispatch, getState in
        return { next in
            return { action in
                print("Action: ", action)
                return next(action)
            }
        }
    }
}

Any thought on that would be appreciated so much :)

New version

New version should be released in order to start using SPM. I would suggest doing a minor bump to 2.3.0.

Support SkipRepeats

Hi,

I wanted to know if this library support or maybe can support SkipRepeats.

What is SkipRepeats + Example:

SkipRepeats allows ReSwift framework to skip and not trigger the newState delegate method in case the sub-state was not changed. It's really simple but very helpful feature that can boost apps performance.

Let's assume that I have the following AppState in my app:

struct AppState : StateType, HasNavigationState {
    var itemState: ItemState,
    var currentUserState: CurrentUserState
}

In order to use skip repeats you need to divide your AppState to sub-states and subscribe to the sub state (or part of it) in the view controller. Each sub-state must conform to Equatable, this way the framework can detect if the sub-state has been changed or not (after dispatching an action) and only if it was changed then the newState method will be called.

So current user state looks like the following:

struct CurrentUserState : StateType {
    var name: String?
    var currentUserId: String?
   .... 
}

extension CurrentUserState : Equatable {
    static func ==(lhs: CurrentUserState, rhs: CurrentUserState) -> Bool {
        return lhs.currentUserId == rhs.currentUserId &&
            lhs.name == rhs.name
    }
}

Now, because ReSwift implemented selectors you can subscribe only to part of your state inside your view controller:

        store.subscribe(self) {subscription in
            subscription.select {state in
                state.currentUserState }.skipRepeats()
        }

As you can see from the code above, If I put skipRepeats() the view controller newState (the subscriber) will be called only if the currentUserState changed.

SkipRepeats is a small but very powerful feature that can lead to big improvements in the performance of an app and especially for a big apps.

I wanted to know if it is possible to support skipRepeats in this library as well or maybe it is already supported and I am missing something?

Thanks in advance!

Make connection bindings work for non Equatable types

Sometimes I need to use a "protocol type" as a prop. Unfortunately, there is an Equatable constraint on the types, that can be used to subscribe/bind.
There fore this is only possible going down the "type erasure road", afaik. But this is just completely overkill for me here.

So I would suggest, adding additional binders without the Equatable constraint, but instead enforce to supply a manual isEqual function. (like you actually do, one step deeper^^)

For testing I just modified the one of the existing binders:

    public func bind<S: Sequence,M>(_ keyPath: KeyPath<Props, S>,
                                    to binder: (Observable<M>) -> Disposable,
                                    isEqual: @escaping (S,S) -> Bool,
                                    mapping: ((S)->M)? = nil)
    {
        let distinctAtKeyPath = self.propsEntry(at: keyPath) { isEqual($0, $1) }

        let afterMapping: Observable<M>
        if let mapping = mapping {
            afterMapping = distinctAtKeyPath.map(mapping)
        } else {
            afterMapping = distinctAtKeyPath as! Observable<M>
        }

        afterMapping
            .bind(to: binder)
            .disposed(by: disposeBag)
    }

What do you think? :-)
If you like it, I can create a Pull request for this, but I just wanted to ask first, before I put more work into this.

Thanks in advance
Martin

Make ReRx-enabled view controllers strictly pure

When using react and redux, there is a module called react-redux that is the inspiration for ReRxSwift. Using that module allows you to cleanly separate stateful and pure components. I would like to make view controllers strictly pure as well, meaning that they have no explicit dependency on anything other than their props and actions. In the current form, this is broken by the requirement to have a connection property in your view controller, which needs the application store as one of its parameters.

ReRxSwift can only be implemented if your view controller has two additional properties: props and actions. The current implementation uses protocol extensions to reduce the requirement to one single connection property; props and actions are forwarded to the connection instance.

In general, properties can be added to classes by defining subclasses, or by using the Objective-C runtime in the form of associated objects. I don't like the subclass-route, because if ReRxSwift would provide a ViewController subclass that you have to use, you can no longer benefit from UITableViewController, UICollectionViewController, etc. And the Objective-C route is not desirable because it breaks Swift strong typing, and because I'm not convinced that it is very future-proof as a similar mechanism doesn't exist in Swift.

Do you have any ideas on how to improve this?

Can update to the least RxSwift?

I like your idea and your project. Hoping It can be maintained continuously.

I meet with some error in SPM fetching:

Showing All Messages
because ReRxSwift >=2.2.2 depends on RxSwift 5.0.0..<6.0.0 and root depends on RxSwift 6.1.0..<7.0.0, ReRxSwift >=2.2.2 is forbidden.
And because root depends on ReRxSwift 2.2.2..<3.0.0, version solving failed.

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.