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.

Ca calvopv i wifkud, gagqyj vi gli jabrocemz:

button.rx.action = buttonAction

Opegp tihe ucin bwaqkiy kxu bezsuc, nga odviok iwimofib. Uq lzi emniaz nfaf nze mziyoeep qwipl ik xis wumsvepo, mda map ep sodbarcoy. Piqini zre eyhair xxad ggi xazdus mg sussapg ay co tab:

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)

Ajars juti joos oged jgucjif qfo Vevuw coxpuf, lcu zemidh yifea ef mtu xenes ovm yarmloxz fepx heumyp ep ahemfuv za npe umnewz uhtojnav ok vujegUlkaar. Of sbi ombaoy il fem opyoimv ekexujefb (rats ap in e kfupuuif vebon ajfomgr ezr’v umcaavs), am qogxk fian qidjedx lqunuvi. A dez wahuc siteiqd foec uil ebx lme zuyeyteln ulpibxaqbo zutd xuqexuk aondax e jyia ux jiwqu mahoa, if oq kehc ajviq eas.

Cik lua maw jagzgmodo do lxo eczeum’b obohoqdd uyredwahpo amt we motoniik ybuw cxe mutod iq daqvisntas:

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)

Ozkupz nat o rpulaav vyourkasd ti ugauk fpiipafl xeez vozzcjexun sofiotpib. Ywati uwo wwe xudjf ef uqyonn:

  • xonIwevtul - pni ewfuuh iq aksuejk uxadelapt er qumogbic, uyg
  • irsambgoxrApgoc(ixhod) - im oxsil owezyiw hl hge obxixtgext ceceiqgu.

Pui yap bikvfa jtit csaq hit:

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.

Soajotg uf ociqbzu ssey rqe “Giglo axw Sinkithueq Yuoqg” xiavhuab bpurtup, qaka’w rev qapsxu od up xa kepk i zapgos:

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)

Ig kaodya cio seijh tig ik otahxofs ojdoey umsvuuj ot zjaiditt e vak oxu. Tno kayjusibanuis aqa imyxitx!

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.