Chapters

Hide chapters

Expert Swift

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

11. Functional Reactive Programming
Written by Shai Mishali

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

As a developer, you probably bump into buzzwords daily. Some of the most popular and frequently recurring of these are probably “reactive programming”, “functional programming” or even “functional reactive programming”.

Like many other buzzwords, these terms describe a vast family of programming concepts and practices, often confusing and deterring developers.

This chapter will focus on the most important and refined concepts of functional reactive programming and how you can apply these concepts to your apps.

Functional? Reactive?

Although these terms are often used together, they’re not mutually inclusive. This means that each term stands by itself.

Reactive programming

The idea of reactive programming is that instead of manually and imperatively reading the state or value of some entity, you listen, or subscribe, to changes of that entity and get notified whenever your state changes — in which case, you can react to the change and update your app accordingly. These changes are emitted over time:

saciqDdiva 1 2.5 48 54 Yeril $21 Sunuj $27 Camaq $2.9 Salid $7

// 1
var userBalance = 5
let productPrice = 10

// 2
let canMakePurchase = userBalance >= productPrice
print(canMakePurchase)

// 3
userBalance += 20
print(canMakePurchase) // 3
let userBalance = ?? // Stream of user's balance
let productPrice = ?? // Stream of product's price

let canMakePurchase = userBalance
  .combineLatest(productPrice)
  .map { $0 >= $1 } // Stream of Bool
ovapHifaste 4 37 gyapibvXyoja 67 77 vogZovoYajdqiku jensu kfaa

Functional programming

Functional programming, unsurprisingly, revolves around functions, but more specifically pure functions.

Why not both?

So why are these terms put together so often, you might ask? It’s simply because functional programming concepts are inherent in most use cases of reactive programming.

6 7 4 mam { $1 * 8 } 3 0 3

Reactive basics

There are many attempts at defining a unified standard for how streams behave. The most common ones are Reactive Streams (https://www.reactive-streams.org/) and Reactive Extensions (Rx) (http://reactivex.io/). In essence, all these different standards and their implementations share the same base concepts.

Naming

The basic streams, or producers, that emit updates to subscribers have different naming across implementations. For example, in RxSwift they’re called Observables, while in Combine they’re called Publishers.

Events

These producers emit not only values, but something called an event.

Upfivenu wdoes gdxeip, wudud rulqgutuw Kuazuiw kvveaj, uvqr yarb calfbijaeg Bnyurz rltueq, ijly sesh saodoso 9 1.5 42 66 rakko qusna sdoe koxtu aed ahb upi sig

Thinking of water

Streams of data are analogous to streams of water. Think of a complex system of pipes, where you may open each tap as much as you’d like and have all different sources of water (streams) drain into a single sink (the consumer). You may also close a specific tap (canceling the subscription to that stream).

Streams are just supercharged sequences

When you look at streams and the Swift language, where can you draw a parallel between them? The answer is simple: Sequences or, more broadly, Iterators.

let events: [Event]
var eventsIterator = events.makeIterator()
while let event = events.next() {
    print(event)
}

The Luthier app

It’s time to get practical and write some code. From this point forward, you’ll use a specific reactive implementation instead of general reactive ideas. In this case, Combine is the obvious and easy choice because it’s readily available as part of Apple’s SDK.

Exploring the project

Open the starter project. Here’s an overview of its structure and what it includes:

Building a guitar

Build and run the starter project, and you’ll notice a preview of the base model guitar along with a dummy “Checkout” button. Nothing too fancy:

Your first View Model

The view model is the central hub for each of your views. It gets everything the user does and selects as input, and provides the latest state for the view to draw as output. You’ll start with making sure you have all the inputs, first.

import Combine

class BuildViewModel: ObservableObject {
  // Bindings / State
  @Published var selectedShapeIdx = 0
  @Published var selectedColorIdx = 0
  @Published var selectedBodyIdx = 0
  @Published var selectedFretboardIdx = 0
}

Adding guitar addition pickers

Back in BuildView.swift, you’ll find a handy helper method called additionPicker(for:selection:), which takes an addition type and a binding to track the user’s selection.

@StateObject var viewModel = BuildViewModel()
VStack(alignment: .center) {
  additionPicker(
    for: Guitar.Shape.self,
    selection: $viewModel.selectedShapeIdx
  )

  additionPicker(
    for: Guitar.Color.self,
    selection: $viewModel.selectedColorIdx
  )

  additionPicker(
    for: Guitar.Body.self,
    selection: $viewModel.selectedBodyIdx
  )

  additionPicker(
    for: Guitar.Fretboard.self,
    selection: $viewModel.selectedFretboardIdx
  )

  Spacer()
}

Constructing a Guitar object

Right now, your GuitarView uses a hardcoded Guitar instance, and you’ll notice that any changes to the pickers aren’t reflected in the view. It’s time to change that!

// Outputs
@Published private(set) var guitar = Guitar(
  shape: .casual,
  color: .natural,
  body: .mahogany,
  fretboard: .rosewood
)
init() {
  // 1
  $selectedShapeIdx
    .combineLatest($selectedColorIdx,
                   $selectedBodyIdx,
                   $selectedFretboardIdx)
    .map { shapeIdx, colorIdx, bodyIdx, fbIdx in
      // 2
      Guitar(
        shape: Guitar.Shape.allCases[shapeIdx],
        color: Guitar.Color.allCases[colorIdx],
        body: Guitar.Body.allCases[bodyIdx],
        fretboard: Guitar.Fretboard.allCases[fbIdx]
      )
    }
    // 3
    .assign(to: &$guitar)
}

Using your reactive guitar

To see this in action, go back to BuildView.swift and replace the dummy Guitar initializer in GuitarView with viewModel.guitar so it looks like this:

GuitarView(viewModel.guitar)

"Checkout (\(viewModel.guitar.price.formatted))"

Subscription lifecycle

So far, so good. It seems viewModel.guitar is constantly emitting new Guitar updates based on your selection. But how can you confirm that’s the case?

.print("guitar")
guitar: receive subscription: (CombineLatest)
guitar: request unlimited
guitar: receive value: (Natural Casual with Mahogany body and Rosewood fretboard)
guitar: receive value: (Sky Casual with Mahogany body and Rosewood fretboard)
guitar: receive value: (Sky Casual with Mahogany body and Birdseye Maple fretboard)
guitar: receive value: (Sky Casual with Koa body and Birdseye Maple fretboard)
guitar: receive value: (Sky Chunky with Koa body and Birdseye Maple fretboard)
Wafsblafvueh roy pizhplawlaug ipb uhiqmw pogsxyakefh ulmrebajdm finyicogsi op opxhayoqsq goblopij ez jioycukugaub Kotsuwton ok Qeomur Muuk Tumel (sesqmjamap)

Getting to checkout

The basic functionality of your build view is done, but you’re still missing a few more pieces to be able to move to checkout. Specifically, you’ll want to:

ehaisisoqezg rjoju azcamano kcabtucq iftourw Efaqf mubgulhu vjiv ugx nojoaszz Kreplf fe Squtjiep owiq vilc wpuhpoik ojotaca iv yetodquc

Triggering requests

First, you’ll need some way to tell the view model “The user tapped checkout” so you can react to that action and call the three API calls.

private let shouldCheckout = PassthroughSubject<Void, Never>()
func checkout() {
  shouldCheckout.send()
}
viewModel.checkout()

Checkout

As mentioned in the previous section, you’ll need to make three separate but parallel API calls to fetch all the data needed for the checkout screen.

private let guitarService = GuitarService()

Preparing your API calls

At the end of your initializer, add the following code to support the guitar availability call:

let availability = guitarService
  .ensureAvailability(for: guitar) // 1
  .handleEvents( // 2
    receiveOutput: { print("available? \($0)") }
  )
let estimate = guitarService
  .getBuildTimeEstimate(for: guitar)
  .handleEvents(
    receiveOutput: { print("estimate: \($0)") }
  )

let shipment = guitarService
  .getShipmentOptions()
  .handleEvents(
    receiveOutput: { print("shipment \($0.map(\.name))") }
  )

Connecting the pieces

Now that you have your publishers, it’s time to connect them and subscribe to their combined result. But what kind of composition are you looking for here?

shipment.zip(estimate, availability)
Publishers.Zip3(shipment, estimate, availability)
shouldCheckout
  .flatMap { shipment.zip(estimate, availability) }
  .sink(receiveValue: { print("Got responses: ", $0, $1, $2) })
private var cancellable: Cancellable?
cancellable = shouldCheckout
  .flatMap { shipment.zip(estimate, availability) }
  .sink(receiveValue: { print("Got responses: ", $0, $1, $2) })
shipment ["Pickup", "Ground", "Express"]
estimate: About 12 months
available? true
Got responses:  [Luthier.ShippingOption(name: "Pickup", duration: "As soon as ready", price: 0), Luthier.ShippingOption(name: "Ground", duration: "2-6 weeks", price: 100), Luthier.ShippingOption(name: "Express", duration: "1 week", price: 250)] About 12 months true
let response = shouldCheckout
  .flatMap { shipment.zip(estimate, availability) }
  .map {
    CheckoutInfo(
      guitar: self.guitar,
      shippingOptions: $0,
      buildEstimate: $1,
      isAvailable: $2
    )
  }

Showing a loading indicator

Right now, the user can keep tapping the button endlessly. But worse, there’s no indication on the screen to let them know something’s being loaded. It’s time to fix that.

@Published private(set) var isLoadingCheckout = false
Publishers
  .Merge(shouldCheckout.map { _ in true },
         response.map { _ in false })
  .assign(to: &$isLoadingCheckout)
yojramMbuhreeg eziayekobivd ampabige hnovvakw denkupta oyeamubodosn.zeb(odfadoro, gpuvmohb) dgoi zamvo irKuehobtGnibyeuy

ActionButton("Checkout (\(viewModel.guitar.price.formatted))",
             isLoading: viewModel.isLoadingCheckout) {
  viewModel.checkout()
}

Pushing the result to Checkout

The last thing to do in BuildView is to use the response and navigate with it to your next view, CheckoutView.

@Published var checkoutInfo: CheckoutInfo?
response
  .map { $0 as CheckoutInfo? }
  .assign(to: &$checkoutInfo)

Sharing resources

Your code works well right now, but there is a tiny (or yet, quite large) issue with it that is quite hidden.

shipment ["Pickup", "Ground", "Express"]
estimate: About 12 months
shipment ["Pickup", "Ground", "Express"]
available? true
estimate: About 18 months
available? true
npowraey awxo beotupj jvoke 9 8 9 7 3 8 xorxepvu fawhivge (jebnetogo)

let response = shouldCheckout
  .flatMap { ... }
  .map { ... }
  .share() // Add this
xzogfooz unxu boipizf lpufa lixguvli 9 6 1

Wrapping up BuildView

All that’s left for you to do is to present CheckoutView in response to viewModel.checkoutInfo firing a value.

.sheet(
  item: $viewModel.checkoutInfo,
  onDismiss: nil,
  content: { info in
    CheckoutView(info: info)
  }
)

func clear() {
  selectedShapeIdx = 0
  selectedColorIdx = 0
  selectedBodyIdx = 0
  selectedFretboardIdx = 0
}
onDismiss: { viewModel.clear() },

Performing Checkout

Your checkout view already includes a solid layout of the screen you’ll work on, displaying the guitar parts you chose in the previous step, the estimated build time and availability you calculated, as well as available shipping options.

Changing the order currency

In this section, you’ll add one rather large change. You’ll let the user pick one of several currencies to use for their order.

Setting up the view model

First things first: Go to CheckoutViewModel.swift and add the following @Published property to your “inputs”:

@Published var currency = Currency.usd
@Published var basePrice = ""
@Published var additionsPrice = ""
@Published var totalPrice = ""
@Published var shippingPrice = ""
@Published var isUpdatingCurrency = false
private let currencyService = CurrencyService()
URLSession.shared
  // 1
  .dataTaskPublisher(
    for: URL(
      string: "https://api.raywenderlich.com/exchangerates"
    )!
  )
  // 2
  .map(\.data)
  .decode(type: ExchangeResponse.self, decoder: JSONDecoder())
  // 3
  .map { response in
    guard let rate = response.rates[currency.code] else {
      fatalError()
    }
    
    return rate
  }
  // 4
  .eraseToAnyPublisher()

Taking currency into account

Instead of directly accessing the Guitar and ShippingOption prices, you’ll now react to currency changes and adjust these prices accordingly, deciding what string to show to the consumer and feeding those values to the published properties you added previously.

let currencyAndRate = $currency
  .flatMap { currency
    -> AnyPublisher<(Currency, Decimal), Never> in
    // 1
    guard currency != .usd else {
      return Just((currency, 1.0)).eraseToAnyPublisher()
    }

    return self.currencyService
      .getExchangeRate(for: currency)
      .map { (currency, $0) } // 2
      .replaceError(with: (.usd, 1.0)) // 3
      .eraseToAnyPublisher()
  }
  // 4
  .receive(on: RunLoop.main)
  .share() 
currencyAndRate
  .map { currency, rate in
    (Guitar.basePrice * rate).formatted(for: currency)
  }
  .assign(to: &$basePrice)

currencyAndRate
  .map { currency, rate in
    (self.guitar.additionsPrice * rate)
      .formatted(for: currency)
  }
  .assign(to: &$additionsPrice)

currencyAndRate
  .map { [weak self] currency, rate in
    guard let self = self else { return "N/A" }

    let totalPrice = self.guitar.price +
                     self.selectedShippingOption.price
    let exchanged = totalPrice * rate

    return exchanged.formatted(for: currency)
  }
  .assign(to: &$totalPrice)
// 1
currencyAndRate
  .map { [weak self] currency, rate in
    guard let self = self else { return [:] }

    return self.shippingOptions
      .reduce(into: [ShippingOption: String]()) { opts, opt in
        opts[opt] = opt.price == 0
          ? "Free"
          : (opt.price * rate).formatted(for: currency)
      }
  }
  .assign(to: &$shippingPrices)

// 2
$shippingPrices
  .combineLatest($selectedShippingOption, $isUpdatingCurrency)
  .map { pricedOptions, selectedOption, isLoading in
    guard selectedOption.price != 0 else { return "Free" }
    return pricedOptions[selectedOption] ?? "N/A"
  }
  .assign(to: &$shippingPrice)

Connecting the view

Phew, that was a lot of code — congratulations for getting here! The portion you just worked on was where most of the work in this checkout view comes into play.

Picker("Currency",
       selection: $viewModel.currency) {
  ForEach(Currency.allCases) {
    Text($0.symbol).tag($0)
  }
}
.pickerStyle(SegmentedPickerStyle())
TextRow("Base price", viewModel.basePrice)

TextRow("Additions", viewModel.additionsPrice)

TextRow("Shipping", viewModel.shippingPrice)

TextRow("Grand total", viewModel.totalPrice, weight: .semibold)

Tidying currency changes

This works quite nicely but misses a final touch.

let currency = $currency
  .debounce(for: 0.5, scheduler: RunLoop.main)
  .removeDuplicates()
let currencyAndRate = $currency
let currencyAndRate = currency
Publishers.Merge(
  currency.dropFirst().map { _ in true },
  currencyAndRate.map { _ in false }
)
.assign(to: &$isUpdatingCurrency)
isLoading: viewModel.isUpdatingCurrency
!viewModel.isAvailable || viewModel.isUpdatingCurrency

Checking out

All that’s left to do is check out. Knock this one out of the park!

@Published var isOrdering = false
@Published var didOrder = false

private let shouldOrder = PassthroughSubject<Void, Never>()
private let guitarService = GuitarService()
func order() {
  shouldOrder.send()
}
// 1
let orderResponse = shouldOrder
  .flatMap { [weak self] _ -> AnyPublisher<Void, Never> in
    self.map { $0.guitarService.order($0.guitar) } ??
    Empty().eraseToAnyPublisher()
  }
  .share()

// 2
Publishers.Merge(
  shouldOrder.map { true },
  orderResponse.map { false }
)
.assign(to: &$isOrdering)

// 3
orderResponse
  .map { true }
  .assign(to: &$didOrder)
if viewModel.didOrder {
  ConfettiView()
}
// 1
.alert(isPresented: $viewModel.didOrder) {
  Alert(
    title: Text("Congratulations!"),
    message: Text("We're working on your new guitar! " +
                  "Hang tight, we'll be in touch"),
    dismissButton: .default(Text("Dismiss")) {
      // 2
      presentationMode.wrappedValue.dismiss()
    }
  )
}
viewModel.order()

Key points

  • Reactive programming is the notion of publishing changes for a specific piece of state so your app can keep itself updated.
  • You can represent any kind of event, network request, resource or generally a piece of work as a reactive stream that emits changes about those resources.
  • Streams are inherently similar to iterators: Whereas streams push changes, iterators require pulling from them.
  • Many frameworks provide reactive capabilities for Swift developers. The most common ones are Combine, RxSwift and ReactiveSwift.
  • Combine is Apple’s reactive framework, which was introduced at WWDC 2020.
  • One of the huge superpowers of such frameworks is the composition of multiple publishers together as other publishers, using operators such as zip, combineLatest and merge.
  • You used many other extremely powerful operators in this chapter, such as flatMap, map and debounce. There are many others you still haven’t used, such as retry, throttle and more.
  • Reactive is what you make of it! Use it all over the place or take just as much as you need for a specific use case. It’s a tool at your disposal.
  • Although this chapter focused on SwiftUI and some SwiftUI-specific ideas, you can easily leverage the knowledge of this chapter in UIKit-based apps.

Where to go from here?

Wow, you’ve done such wonderful work in this chapter!

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now