Getting Started With Combine

When Apple added the Observable protocol to iOS 17 it looked like the end for the Combine framework. Here’s my gentle introduction if you still need it.

Last updated: Feb 20, 2024

Getting Started with Combine

There are a lot of good resources to help you get started with Combine but what worked for me was the plain old WWDC 2019 sessions (Introducing Combine and Combine in Practice). I recommend watching the two sessions back to back.

I’m going to assume you’ve watched those sessions but even if you haven’t you’ll probably get the idea from the sample code below.

A Practical Example - Validating User Input

I wanted a simple example that would give me some practise in applying Combine without getting lost in detail. I thought I would follow the approach from the Apple WWDC sessions and apply it to validating user input. Here’s my user interface:

User interface with two switches (both off), a text field (empty) and a submit button (disabled)

The challenge is to only enable the Submit button when the user has activated both switches and entered their name in the text field. My starting point is a view controller with outlets wired up to the storyboard controls:

// TermsViewController.swift
import UIKit

final class TermsViewController: UIViewController {
  @IBOutlet private var termsSwitch: UISwitch!
  @IBOutlet private var privacySwitch: UISwitch!
  @IBOutlet private var nameField: UITextField!
  @IBOutlet private var submitButton: UIButton!
}

The first step is add some properties to hold the internal state:

private var acceptedTerms: Bool = false
private var acceptedPrivacy: Bool = false
private var name: String = ""

Then in the target-action methods for the two switches and the text field we set the properties when the user changes something:

@IBAction private func acceptTerms(_ sender: UISwitch) {
    acceptedTerms = sender.isOn
}

@IBAction private func acceptPrivacy(_ sender: UISwitch) {
    acceptedPrivacy = sender.isOn
}

@IBAction private func nameChanged(_ sender: UITextField) {
    name = sender.text ?? ""
}

What I would normally do at this point is create a method (probably called configureView) that would enable or disable the button based on the values of my three properties. I would call that method from a didSet observer for each property:

private var acceptedTerms: Bool = false {
  didSet { configureView() }
}

I might not normally even bother with the properties but I know I’m going to need them in a minute. There’s nothing wrong with this approach, but I’m trying to learn Combine. Let’s walk through my attempt.

Import the framework

Baby steps first:

import Combine

Make properties into publishers

I need to convert my simple properties into publishers. We do that by adding the @Published keyword to each property:

@Published private var acceptedTerms: Bool = false
@Published private var acceptedPrivacy: Bool = false
@Published private var name: String = ""

The @Published keyword is an example of property wrapper added with Swift 5.1. It creates a publisher for any property of a class (you can’t use it with value types like struct). You can still set and get the normal property but you can also access the publisher wrapper using a $ prefix.

For example, to subscribe to the publisher for the acceptedTerms property:

// Keep a reference to the subscription so it's
// only cancelled when we are deallocated.
private var termsStream: AnyCancellable?   

// Simple sink subscription to print value
termsStream = $acceptedTerms.sink { value in
  print("Received \(value)")
}

The type of the two boolean publishers is Published<Bool>.Publisher and the name publisher is of type Published<String>.Publisher. Anytime the user enters text or moves a switch the new value is available to any subscriber of these publishers.

Combine Operators

This is where we get to the combine bit of using combine. To determine when we can enable the submit button we need several things to be true:

  • The name must not be blank.
  • Both switches should be on.

The combineLatest operator is useful here. It gives us a new publisher that receives and combines the latest values from other publishers. There are variations to combine two, three or four publishers. Using the version that accepts three publishers:

private var validToSubmit: AnyPublisher<Bool, Never> {
  return Publishers.CombineLatest3($acceptedTerms, $acceptedPrivacy, $name)
    .map { terms, privacy, name in
      terms && privacy && !name.isBlank
    }.eraseToAnyPublisher()
}

There’s a lot going on here so let’s break it down

CombineLatest

First we combine the latest values of our three publishers (note we are using the $ prefix to get the wrapped version of each property):

Publishers.CombineLatest3($acceptedTerms, $acceptedPrivacy, $name)

This gives us a tuple of type (Bool, Bool, String). A subscriber to this publisher receives the new tuple of latest values when any of the properties changes.

Map Operator

Next we use a map operation to convert our tuple to a single Bool indicating if we can enable the submit button:

.map { terms, privacy, name in
  terms && privacy && !name.isBlank
}

The closure takes the three values from the tuple and returns true if both the boolean properties are true and the name string is not blank. I’m using a small String extension to test a String for whitespace - see Empty Strings in Swift.

Type Erasure

Finally I am using the type erasure method eraseToAnyPublisher() to wrap the real type of this publisher so that any subscriber only sees the generic type AnyPublisher<Bool, Never>:

}.eraseToAnyPublisher()

All a subscriber should care about is that we publish a Bool and never return an error. They should not care how we produce that result. The publisher type will change depending on how we combine the upstream publishers. In this case the real type of validToSubmit is this unwieldy mouthful:

Publishers.Map<Publishers.CombineLatest3<Published<Bool>.Publisher,
 Published<Bool>.Publisher, Published<String>.Publisher>, Bool>

You can attempt to read this as we map the combined latest values of two Bool and a String publisher to a Bool. Life is easier for everybody if we hide the internal details and reset the type to AnyPublisher<Bool, Never>.

Keypath Assign Subscription

Almost done. Finally we create the subscription to receive from our publisher and set the state of the submit button. We need a property to keep a reference to the subscription stream so it doesn’t get cancelled until the view controller goes away:

private var stream: AnyCancellable?

Then in viewDidLoad we create the subscription to our publisher:

stream = validToSubmit
  .receive(on: RunLoop.main)
  .assign(to: \.isEnabled, on: submitButton)

Since we’re interacting with UIKit, we use receive(on:) to make sure we receive from our publisher on the main run loop. The assign(to:) operator assigns the received value to a keypath of an object. We use it to assign the received Bool to the isEnabled property of the submit button.

The final result with both switches on and some non-blank text in the name field. The submit button is now showing it’s green enabled state:

Both switches are on and the text field contains text. The submit button is green (enabled)

It takes some getting use to but it’s not much more code. I’m not going to rewrite everything just to use Combine but this is a gentle way to learn the basics.

Here’s the final view controller code:

// TermsViewController.swift
import Combine
import UIKit

final class TermsViewController: UIViewController {
  @IBOutlet private var termsSwitch: UISwitch!
  @IBOutlet private var privacySwitch: UISwitch!
  @IBOutlet private var nameField: UITextField!
  @IBOutlet private var submitButton: UIButton!

  @Published private var acceptedTerms: Bool = false
  @Published private var acceptedPrivacy: Bool = false
  @Published private var name: String = ""

  private var stream: AnyCancellable?

  override func viewDidLoad() {
    super.viewDidLoad()
    stream = validToSubmit
      .receive(on: RunLoop.main)
      .assign(to: \.isEnabled, on: submitButton)
  }

  @IBAction private func acceptTerms(_ sender: UISwitch) {
    acceptedTerms = sender.isOn
  }

  @IBAction private func acceptPrivacy(_ sender: UISwitch) {
    acceptedPrivacy = sender.isOn
  }

  @IBAction private func nameChanged(_ sender: UITextField) {
    name = sender.text ?? ""
  }

  @IBAction private func submitAction(_ sender: UIButton) {
    print("Submit... \(name)")
  }

  private var validToSubmit: AnyPublisher<Bool, Never> {
    return Publishers.CombineLatest3($acceptedTerms, $acceptedPrivacy, $name)
      .map { terms, privacy, name in
        terms && privacy && !name.isBlank
    }.eraseToAnyPublisher()
  }
}

Get the Code

The full code for this post is in my GitHub repository:

Learn More

I found the two Apple sessions from WWDC 2019 to be an excellent introduction to Combine: