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:
props
and from ReSwift
actions to actions
. (See also section 'Open Issues' below.)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.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
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.
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
}
}
Define how your state is mapped to the above Props
type:
private let mapStateToProps = { (appState: AppState) in
return MyViewController.Props(
text: appState.content
)
}
Define the actions that are dispatched:
private let mapDispatchToActions = { (dispatch: @escaping DispatchFunction) in
return MyViewController.Actions(
updatedText: { newText in dispatch(SetContent(newContent: newText)) }
)
}
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()
}
}
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)
}
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
.
Below is a high-level description of the most important components of the API. There is also full API documentation of ReRxSwift available.
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.
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.
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
.
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
.
To create your Connection
instance, you need to construct it with three
parameters:
props
object. This decouples
your application state from the view controller data.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.
Calling this method causes the connection to unsubscribe from the ReSwift store.
Call this from your view controller's viewWillDisappear
or viewDidDisappear
.
Subscribe to an entry in your view controller's props
, meaning that the given
onNext
handler will be called whenever that entry changes.
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:
props
that you want to bind to the user interface element.textField.rx.text
or
progressView.rx.progress
.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)
The folder Example
contains the following examples:
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
.
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.
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
!)