Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

8. Observer Pattern
Written by Joshua Greene

The observer pattern lets one object observe changes on another object. Apple added language-level support for this pattern in Swift 5.1 with the addition of Publisher in the Combine framework.

This pattern involves three types:

  1. The subscriber is the “observer” object and receives updates.
  2. The publisher is the “observable” object and sends updates.
  3. The value is the underlying object that’s changed.

Note: this chapter provides a high-level introduction to @Published properties, but it doesn’t get into all of the details or powerful features offered in the Combine framework. If you’d like to learn more Combine, see our book Combine: Asynchronous Programming with Swift (http://bit.ly/swift-combine).

When should you use it?

Use the observer pattern whenever you want to receive changes made on another object.

This pattern is often used with MVC, where the view controller has subscriber(s) and the model has publisher(s). This allows the model to communicate changes back to the view controller without needing to know anything about the view controller’s type. Thereby, different view controllers can use and observe changes on the same model type.

Playground example

Open FundamentalDesignPattern.xcworkspace in the Starter directory, or continue from your own playground workspace from the last chapter, and then open the Overview page.

You’ll see Observer is listed under Behavioral Patterns. This is because Observer is about one object observing another object.

Click on the Observer link to open that page.

Then, enter the following below Code Example:

// 1
import Combine

// 2
public class User {
    
    // 3
    @Published var name: String
    
    // 4
    public init(name: String) {
        self.name = name
    }
}

Here’s what you did:

  1. First, you import Combine, which includes the @Published annotation and Publisher & Subscriber types.

  2. Next, you declare a new User class; @Published properties cannot be used on structs or any other types besides classes.

  3. Next, you create a var property for name and mark it as @Published. This tells Xcode to automatically generate a Publisher for this property. Note that you cannot use @Published for let properties, as by definition they cannot be changed.

  4. Finally, you create an initializer that sets the initial value of self.name.

Next, add the following code to the end of the playground:

// 1
let user = User(name: "Ray")

// 2
let publisher = user.$name

// 3
var subscriber: AnyCancellable? = publisher.sink() {
    print("User's name is \($0)")
}

// 4
user.name = "Vicki"

Here’s what you did:

  1. First, you create a new user named Ray.

  2. Next, you access the publisher for broadcasting changes to the user’s name via user.$name. This returns an object of type Published<String>.Publisher. This object is what can be listened to for updates.

  3. Next, you create a subscriber by calling sink on the publisher. This takes a closure for which is called for the initial value and anytime the value changes.

    By default, sink returns a type of AnyCancellable. However, you explicitly declare this type as AnyCancellable? to make it optional as you’ll nil it out later.

  4. Finally, you change the user’s name to Vicki.

In response, you should see the following printed to the console:

User's name is Ray
User's name is Vicki

Add the following code next:

subscriber = nil
user.name = "Ray has left the building"

By setting the subscriber to nil, it will no longer receive updates from the publisher. To prove this, you change the user’s name a final time, but you won’t see any new output in the console.

What should you be careful about?

Before you implement the observer pattern, define what you expect to change and under which conditions. If you can’t identify a reason for an object or property to change, you’re likely better off not declaring it as var or @Published, and instead, making it a let property.

A unique identifier, for example, isn’t useful as an published property since by definition it should never change.

Tutorial project

You’ll continue the Rabble Wabble app from the previous chapter.

If you skipped the previous chapter, or you want a fresh start, open Finder and navigate to where you downloaded the resources for this chapter. Then, open starter ▸ RabbleWabble ▸ RabbleWabble.xcodeproj in Xcode.

You’ll use the observer pattern to display the user’s latest score on the “Select Question Group” screen.

Open QuestionGroup.swift from the File hierarchy. This already has a Score, but it’s not currently possible to observe changes on it. Add the following below import Foundation:

import Combine

This imports Apple’s new Combine framework to do all the heavy lifting for you.

Next, add the following to the end of the Score class (which is inside the QuestionGroup class) after its other properties, ignorning the compiler error for now:

@Published public var runningPercentage: Double = 0

The runningPercentage property will allow the question group’s latest “running percentage score” to be observed.

The compiler is currently throwing an error because the property is marked as @Published, and it doesn’t know how to automatically encode or decode it.

To fix this, add the following code right after init for Score:

// 1
private enum CodingKeys: String, CodingKey {
  case correctCount
  case incorrectCount
}

// 2
public required init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  self.correctCount = try container.decode(Int.self, forKey: .correctCount)
  self.incorrectCount = try container.decode(Int.self, forKey: .incorrectCount)
  updateRunningPercentage()
}

// 3
private func updateRunningPercentage() {
  let totalCount = correctCount + incorrectCount
  guard totalCount > 0 else {
    runningPercentage = 0
    return
  }
  runningPercentage = Double(correctCount) / Double(totalCount)
}

Here’s what this does:

  1. You first declare an enum for CodingKeys and cases for correctCount and incorrectCount. This tells the compiler to ignore runningPercentage in the encoder and decoder methods it automatically generates.

  2. In the event that a Score is decoded, you need to actually set runningPercentage. To do this, you create a custom initializer for init(from decoder:), and call updateRunningPercentage() after setting correctCount and incorrectCount.

  3. Within updateRunningPercentage(), you set the runningPercentage based on the ratio of correctCount to totalCount.

Next, replace the var correctCount and var incorrectCount lines with the following:

public var correctCount: Int = 0 {
  didSet { updateRunningPercentage() }
}
public var incorrectCount: Int = 0 {
  didSet { updateRunningPercentage() }
}

While you could have marked correctCount and incorrectCount as @Published, you’re not interested in observing these properties individually. Rather, you’re interested in how they affect runningPercentage. So within didSet for each of these, you call updateRunningPercentage().

Before you can start creating subscribers for runningPercentage, you need to make a few small changes. First, add the following method to the end of the Score class before the closing curly brace:

public func reset() {
  correctCount = 0
  incorrectCount = 0
}

This method “resets” Score. You’ll use it whenever the user restarts a QuestionGroup.

Next, replace the var score line with the following, ignoring the resulting compiler error:

public private(set) var score: Score

This prevents all outside classes from setting score directly. This ensures any runningPercentage subscribers aren’t accidentally wiped out, which would happen if score was set directly.

There’s currently one place that does set score directly. Open BaseQuestionStrategy.swift and replace the following line:

self.questionGroupCaretaker.selectedQuestionGroup.score =
  QuestionGroup.Score()

…with the following:

self.questionGroupCaretaker.selectedQuestionGroup.score.reset()

Build and run to ensure you don’t have any compiler errors. Nothing appears to have changed so far, but you’re now ready to register your observers!

You first need somewhere to hold onto the subscriber object. Ideally, this should be tied to the life of the object that it’s related. In this case, this is the QuestionGroupCell itself. Open QuestionGroupCell.swift and add the following below import UIKit:

import Combine

Next, add the following property after the others:

public var percentageSubscriber: AnyCancellable?

Then, open SelectQuestionGroupViewController.swift and add the following code to tableView(_:cellForRowAt:), just before the return statement:

cell.percentageSubscriber =
  questionGroup.score.$runningPercentage // 1
    .receive(on: DispatchQueue.main) // 2
    .map() { // 3
      return String(format: "%.0f %%", round(100 * $0))
  }.assign(to: \.text, on: cell.percentageLabel) // 4

Here’s how this works:

  1. Set cell.percentageSubscriber to the subscriber that’s created. Consequently, if the cell gets released, its subscriber will automatically get released too, and it won’t receive updates.

  2. Call receive(on:) and pass DispatchQueue.main to ensure events are delivered on the main queue. While there’s not any background threads currently used in the app, it’s a good idea to always ensure your UI calls are made on the main queue to prevent future issues.

  3. Transform the value into a percentage string using a map.

  4. Call assign to set the value to text on the cell.percentageLabel. Whenever the value changes, this will automatically update the label’s text too.

Build and run, pick any question group cell you’d like, and tap the “Correct” and “Incorrect” buttons a few times. When you press the “Menu” button, the score will now be visible. Even better, if you quit the app and restart it, the scores will be persisted thanks to your implementation of the memento pattern from the previous chapter.

Key points

You learned about the observer pattern in this chapter. Here are its key points:

  • The observer pattern lets one object observe changes on another object. It involves three types: the subscriber, publisher and value.

  • The subscriber is the observer; the publisher is the observable object; and the value is the object that’s being changed.

  • Swift 5.1 makes it easy to implement the observer pattern using @Published properties.

RabbleWabble is becoming ever more feature-rich. However, there’s one feature that would be really great: the ability for users to create their own QuestionGroups. You’ll use another pattern to do this: the builder design pattern.

Continue onto the next chapter to learn about the builder pattern and complete the RabbleWabble app.

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.
© 2026 Kodeco Inc.