20.
Action
Written by Florent Pillet
A project living under the RxSwiftCommunity organization, Action is an important building block for reactive applications. Thinking about what actions are in your code, the definition is along the lines of:
- A trigger event signals that it’s time to do something.
- A task is performed.
- Immediately, later (or maybe never!), some value results from performing this task.
Notice a pattern? The trigger event can be represented as an observable sequence of something, such as button taps, timer ticks, or gestures, which may or may not convey data, but always signals work to be done. The result of each action can therefore be seen as a sequence of results, one result for each piece of work performed.
In the middle sits the Action object. It does the following:
-
Provides an
inputsobserver to bind observable sequences to. You can also manually trigger new work. -
Can observe an
Observable<Bool>to determine its “enabled” status (in addition to whether it’s currently executing). -
Calls your factory closure which performs / starts the work and returns an observable of results.
-
Exposes an
elementsobservable sequence of all work results (aflatMapof all work observables). -
Gracefully handles errors emitted by work observables.
Action exposes observables for errors, the current execution status, an observable of each work observable, guarantees that no new work starts when the previous has not completed, and it’s generally such a cool class that you don’t want to miss it!
Last but not least, Action defines a contract, where you provide some (or no) data, then some work is done and you may later get resulting data. How this contract is implemented doesn’t matter to the code using the action. You can replace real actions with mock ones for testing without impacting the code at all, as long as the mock respects the contract.
Creating an Action
Action is a generic class defined as class Action<Input, Element>. Input is the type of the input data provided to your factory worker function. Element is the type of element emitted by the observable your factory function returns.
The simplest example of an action takes no input, performs some work and completes without producing data:
let buttonAction: Action<Void, Void> = Action {
print("Doing some work")
return Observable.empty()
}
This is dead simple. What about an action which takes credentials, performs a network request and returns a “logged in” status?
let loginAction: Action<(String, String), Bool> = Action { credentials in
let (login, password) = credentials
// loginRequest returns an Observable<Bool>
return networkLayer.loginRequest(login, password)
}
Note: Each execution of an
Actionis considered complete when the observable returned by your factory closure completes or errors. This prevents starting multiple long-running actions. This behavior is handy with network requests, as you’ll see below.
Action looks cool but it might not be immediately obvious how useful it is in a variety of contexts, so let’s have a look at few practical examples.
Connecting buttons
Action comes with reactive extensions for UIButton and several other UIKit components. It also defines CocoaAction, a typealias for Action<Void, Void> — perfect for buttons which don’t expect an output.
To connect a button, simply do the following:
button.rx.action = buttonAction
Every time user presses the button, the action executes. If the action from the previous press is not complete, the tap is dismissed. Remove the action from the button by setting it to nil:
button.rx.action = nil
Composing behavior
Let’s consider loginAction again from the Creating an Action example above. Connect it to your UI like this:
let loginPasswordObservable = Observable.combineLatest(loginField.rx.text, passwordField.rx.text) {
($0, $1)
}
loginButton.rx.tap
.withLatestFrom(loginPasswordObservable)
.bind(to: loginAction.inputs)
.disposed(by: disposeBag)
Every time your user presses the Login button, the latest value of the login and password text fields is emitted to the inputs observer of loginAction. If the action is not already executing (such as if a previous login attempt isn’t ongoing), it calls your factory closure. A new login request goes out and the resulting observable will deliver either a true or false value, or it will error out.
Now you can subscribe to the action’s elements observable and be notified when the login is successful:
loginAction.elements
.filter { $0 } // only keep "true" values
.take(1) // just interested in first successful login
.subscribe(onNext: {
// login complete, push the next view controller
})
.disposed(by: disposeBag)
Errors get a special treatment to avoid breaking your subscriber sequences. There are two kinds of errors:
-
notEnabled- the action is already executing or disabled, and -
underlyingError(error)- an error emitted by the underlying sequence.
You can handle them this way:
loginAction
.errors
.subscribe(onError: { error in
guard case .underlyingError(let err) = error else {
return
}
// update the UI to warn about the error
}
})
.disposed(by: disposeBag)
Passing work items to cells
Action helps solve a common problem: how to connect buttons in UITableView cells. Action to the rescue! When configuring a cell, you assign an action to a button. This way you don’t need to put actual work inside your cell subclasses, helping enforce a clean separation — even moreso if you’re using an MVVM architecture.
Reusing an example from the “Table and Collection Views” cookbook chapter, here’s how simple it is to bind a button:
observable.bind(to: tableView.rx.items) {
(tableView: UITableView, index: Int, element: MyModel) in
let cell = tableView.dequeueReusableCell(withIdentifier: "buttonCell", for: indexPath)
cell.button.rx.action = CocoaAction { [weak self] in
// do something specific to this cell here
return .empty()
}
return cell
}
.disposed(by: disposeBag)
Of course you could set an existing action instead of creating a new one. The possibilities are endless!
Manual execution
To manually execute an action, call its execute(_:) method, passing it an element of the action’s Input type:
loginAction
.execute(("john", "12345"))
.subscribe(onNext: {
// handle return of action execution here
})
.disposed(by: disposeBag)
Perfectly suited for MVVM
If you’re using MVVM (see Chapter 24, “MVVM with RxSwift” and Chapter 25, “Building a Complete RxSwift app”) you may have figured out by now that RxSwift is very well-suited for this architectural pattern. Action is a perfect match too! It nicely complements the separation between your View Controller and View Model. Expose your data as observables and all actionable functionality as Action to achieve MVVM bliss!