Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

20. Action
Written by Florent Pillet

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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 inputs observer 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 elements observable sequence of all work results (a flatMap of 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 Action is 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.

button.rx.action = buttonAction
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)
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)
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.

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)

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!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now