Booking Dashboard with Design Patterns

Build a UIKit booking dashboard using SOLID principles and SwiftData.

Introduction

30 Second Summary

When a booking dashboard needs to sort by date, then by guest name, then by status priority, the naive approach is to keep adding switch cases to the same function. Before long, one class does everything and testing any single piece means dragging the whole thing along.

In this project, you will build a partner-facing booking dashboard in UIKit backed by SwiftData, then progressively refactor it using SOLID principles and classic design patterns so you can walk your interviewer through every decision.

What You'll Build

You will tap a sort segment, watch the booking rows animate into a new order instantly, and then open Xcode's console to see every operation logged without a single line of logging code inside your business logic.

By the end of this project, you'll have:

  • A booking list persisted in SwiftData and rendered through a UITableViewDiffableDataSource that animates row changes whenever you switch the sort order.
  • Three swappable sort strategies (by date, by guest name, by status priority) you can switch at runtime via a segmented control, without touching existing code.
  • A Decorator that wraps data-fetching to add timestamped logging, proving the Open/Closed Principle in action.
  • Concrete interview talking points for every pattern: the limitation you hit first, the clean solution you applied, and the tradeoff you would explain to Jens.
  • Secret Mission: Write a unit test with a mock repository and custom sort strategy that proves the architecture is testable in complete isolation.

Are there any prerequisites?

This project assumes you have completed the iOS Refactoring Interview Simulator project, where you worked with MVVM, protocol-based networking, dependency injection, and unit testing. You should also be comfortable writing programmatic UIKit interfaces (no storyboards).

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for your Round 1 interview with Jens.

Set Up SwiftData Model and Diffable Data Source

You need a working booking list before you can demonstrate any design pattern. Starting with SwiftData and a UITableViewDiffableDataSource gives you the foundation that every subsequent refactoring builds on.

In this step, you will create the Xcode project, define a SwiftData model for bookings, and wire up a diffable data source that displays 10 seeded reservations in a table view.

In this step, get ready to:
  • Create a new Xcode project with programmatic UI (no storyboard).
  • Define a SwiftData model and a Hashable value type for the diffable data source.
  • Build a table view that displays 10 seeded bookings with colored status badges.
Create the Xcode project and configure programmatic UI

The project uses UIKit with a SceneDelegate-based lifecycle, with no storyboard. You will create the project, delete the storyboard, and set up the window programmatically.

  • Open Xcode and select File in the menu bar.
  • Click New, then Project.
  • Select iOS at the top, then choose App and click Next.
  • Set the Product Name to BookingDashboard.
  • Set Interface to Storyboard and Language to Swift.
  • Save the project on your Desktop and click Create.

Now remove the storyboard and configure programmatic UI.

  • In the Project Navigator, right-click Main.storyboard and select Delete. Choose Move to Trash.
  • Click your project name at the top of the Project Navigator, then select the BookingDashboard target.
  • Go to the Info tab.
  • Under Application Scene Manifest, expand Scene Configuration, then expand the first item.
  • Delete the Storyboard Name row by clicking the minus button next to it.
  • Switch to the Build Settings tab. Search for Main storyboard in the search bar.
  • Delete the value next to UIKit Main Storyboard File Base Name so it is blank.

Why remove the storyboard?

Programmatic UI gives you full control over how views are created and composed. It also makes dependency injection straightforward since you control every initializer.

  • Open SceneDelegate.swift in the Project Navigator.
  • Replace the entire file contents with the following code to set up a programmatic window:
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)

        // Temporary placeholder VC to verify the window works
        let viewController = UIViewController()
        viewController.view.backgroundColor = .systemBackground
        window?.rootViewController = viewController
        window?.makeKeyAndVisible()
    }
}

What does this code do?

The scene(_:willConnectTo:options:) method is the entry point for a SceneDelegate-based app. It creates a window attached to the window scene and sets a root view controller.

You are using a temporary plain UIViewController to verify the window works before building the real one.

  • Also delete ViewController.swift from the Project Navigator (the template-generated one). Choose Move to Trash.
  • Press Cmd+R to run the app in the Simulator.

You should see a blank white screen. This confirms the programmatic window is set up correctly.

Seeing a black screen or a crash?

  • Make sure you deleted the Storyboard Name entry from the Info tab AND cleared the Build Settings value. Both must be removed.
  • Confirm SceneDelegate.swift has the window?.makeKeyAndVisible() call at the end.

Why is my app showing a black screen after removing the storyboard?

Define the SwiftData model and BookingItem struct

The app needs two types: a SwiftData @Model class for persistence, and a lightweight Hashable struct that the diffable data source uses as its item identifier.

  • Create a new Swift file by pressing Cmd+N. Select Swift File and name it Booking.swift.
  • Replace the file contents with the SwiftData model:
import Foundation
import SwiftData

// SwiftData model stored in the persistent container
@Model
final class Booking {
    var id: UUID
    var guestName: String
    var propertyName: String
    var checkIn: Date
    var status: String

    init(id: UUID = UUID(), guestName: String, propertyName: String, checkIn: Date, status: String) {
        self.id = id
        self.guestName = guestName
        self.propertyName = propertyName
        self.checkIn = checkIn
        self.status = status
    }
}

What does this code do?

The @Model macro tells SwiftData to persist this class. Each property becomes a column in the underlying SQLite store.

The status property is a plain String instead of an enum because SwiftData stores raw values. You will map it to a typed enum in the next file.

  • Create another Swift file (Cmd+N) and name it BookingItem.swift.
  • Add the struct definition with the enum cases:
import UIKit

// Hashable value type for the diffable data source
struct BookingItem: Hashable {
    let id: UUID
    let guestName: String
    let propertyName: String
    let checkIn: Date
    let status: BookingStatus

    enum BookingStatus: String, CaseIterable {
        case confirmed
        case checkedIn = "checked_in"
        case checkedOut = "checked_out"
        case cancelled
        case pending
    }
}

Why a separate Hashable struct?

SwiftData @Model objects are not Sendable under Swift 6 strict concurrency. Using them directly as the diffable data source's ItemIdentifierType causes compiler errors.

The recommended pattern is to map @Model instances to plain Hashable structs for the UI layer.

  • Now add the computed properties inside the BookingStatus enum, below case pending:
        // Human-readable label for each status
        var displayName: String {
            switch self {
            case .confirmed: return "Confirmed"
            case .checkedIn: return "Checked In"
            case .checkedOut: return "Checked Out"
            case .cancelled: return "Cancelled"
            case .pending: return "Pending"
            }
        }

        // Color shown next to the status in the table cell
        var displayColor: UIColor {
            switch self {
            case .confirmed: return .systemGreen
            case .checkedIn: return .systemBlue
            case .checkedOut: return .secondaryLabel
            case .cancelled: return .systemRed
            case .pending: return .systemOrange
            }
        }

        // Priority order for status-based sorting (used later)
        var sortPriority: Int {
            switch self {
            case .checkedIn: return 0
            case .confirmed: return 1
            case .pending: return 2
            case .checkedOut: return 3
            case .cancelled: return 4
            }
        }

What do these properties do?

  • displayName provides a clean label ("Checked In" instead of "checked_in") for the cell.
  • displayColor returns a system color so each status gets a distinct visual badge.
  • sortPriority assigns a numeric rank you will use in Step 4 for status-based sorting.
  • Save both files (Cmd+S).
  • Press Cmd+B to build the project.

The build succeeds with no errors, confirming your model layer compiles correctly.

Build failing with a SwiftData error?

  • Confirm Booking.swift has import SwiftData at the top. The @Model macro lives in that framework.
  • Check that both files are added to the BookingDashboard target (select the file, open the File Inspector, verify Target Membership is checked).

Help me fix a SwiftData build error in my Booking model.

Build the table view with a diffable data source

With the model layer in place, you can now create the view controller that fetches bookings from SwiftData and displays them through a diffable data source.

  • Create a new Swift file (Cmd+N) named BookingsViewController.swift.
  • Add the class skeleton with properties and the initializer:
import UIKit
import SwiftData

final class BookingsViewController: UIViewController {
    // Single section for the diffable data source
    enum Section { case main }

    private let modelContext: ModelContext
    private var bookings: [BookingItem] = []

    // Table view fills the entire screen
    private let tableView = UITableView(frame: .zero, style: .plain)

    init(modelContext: ModelContext) {
        self.modelContext = modelContext
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

What does this code do?

  • The Section enum has a single case because the table has one flat list of bookings.
  • The modelContext is injected through the initializer, following Dependency Inversion.
  • The required init?(coder:) fatalError prevents accidental storyboard instantiation.
  • Add the diffable data source, viewDidLoad, and setupUI inside the class, below the initializer:
    // Cell provider closure configures each row
    private lazy var dataSource: UITableViewDiffableDataSource<Section, BookingItem> = {
        UITableViewDiffableDataSource<Section, BookingItem>(tableView: tableView) { tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "BookingCell", for: indexPath)
            let formatter = DateFormatter()
            formatter.dateStyle = .medium
            formatter.timeStyle = .none
            var content = cell.defaultContentConfiguration()
            content.text = "\(item.guestName) - \(item.propertyName)"
            content.secondaryText = "\(formatter.string(from: item.checkIn)) | \(item.status.displayName)"
            content.secondaryTextProperties.color = item.status.displayColor
            cell.contentConfiguration = content
            return cell
        }
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Bookings"
        view.backgroundColor = .systemBackground
        setupUI()
        Task { await loadBookings() }
    }

    private func setupUI() {
        tableView.frame = view.bounds
        tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "BookingCell")
        view.addSubview(tableView)
    }

What does the data source do?

The UITableViewDiffableDataSource takes a cell provider closure that configures each row. It uses defaultContentConfiguration() to set the primary text (guest + property) and secondary text (date + status) with a colored label.

The lazy var ensures the table view exists before the data source references it.

  • Add the data-loading and snapshot methods below setupUI(), still inside the class:
    // Fetch from SwiftData and map to BookingItem
    private func loadBookings() async {
        let descriptor = FetchDescriptor<Booking>()
        do {
            let raw = try modelContext.fetch(descriptor)
            bookings = raw.map { BookingItem(id: $0.id, guestName: $0.guestName, propertyName: $0.propertyName, checkIn: $0.checkIn, status: BookingItem.BookingStatus(rawValue: $0.status) ?? .pending) }
            applySnapshot()
        } catch {
            print("Failed to load bookings: \(error)")
        }
    }

    // Push current bookings array into the diffable data source
    private func applySnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, BookingItem>()
        snapshot.appendSections([.main])
        snapshot.appendItems(bookings)
        dataSource.apply(snapshot, animatingDifferences: true)
    }

What does this code do?

  • loadBookings() uses a FetchDescriptor to query all Booking records from SwiftData, then maps each one to a BookingItem struct.
  • applySnapshot() creates a fresh snapshot, appends all items, and hands it to the data source. The diffable data source handles all row insertions and animations automatically.
  • Save BookingsViewController.swift (Cmd+S).
  • Press Cmd+B to confirm the file compiles.

The build succeeds. Now wire everything together in SceneDelegate.

  • Open SceneDelegate.swift and replace the entire file with the following code that creates the ModelContainer, seeds data, and injects the context:
import UIKit
import SwiftData

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)

        // Create SwiftData container and get the main context
        let container = try! ModelContainer(for: Booking.self)
        let context = container.mainContext
        seedDataIfNeeded(context: context)

        // Inject context into the view controller
        let viewController = BookingsViewController(modelContext: context)
        window?.rootViewController = UINavigationController(rootViewController: viewController)
        window?.makeKeyAndVisible()
    }
}

What does this code do?

  • ModelContainer(for: Booking.self) creates the SwiftData persistence stack for the Booking model.
  • container.mainContext returns a MainActor-aligned context suitable for UI work.
  • The context is injected into BookingsViewController through its initializer, keeping dependencies explicit.
  • Add the seedDataIfNeeded method inside the SceneDelegate class, below the scene method:
    // Seed 10 sample bookings on first launch
    private func seedDataIfNeeded(context: ModelContext) {
        let descriptor = FetchDescriptor<Booking>()
        let count = (try? context.fetchCount(descriptor)) ?? 0
        guard count == 0 else { return }

        let guests = ["Alice Johnson", "Bob Smith", "Carol White", "David Brown", "Eva Martinez",
                      "Frank Lee", "Grace Kim", "Henry Wilson", "Iris Chen", "Jack Davis"]
        let properties = ["Grand Hotel Downtown", "Seaside Resort", "Mountain Lodge",
                          "City Center Inn", "Lakefront Suites"]
        let statuses = ["confirmed", "checked_in", "checked_out", "cancelled", "pending"]

        for i in 0..<10 {
            let booking = Booking(
                guestName: guests[i],
                propertyName: properties[i % properties.count],
                checkIn: Date().addingTimeInterval(Double(i) * 86400),
                status: statuses[i % statuses.count]
            )
            context.insert(booking)
        }
        try? context.save()
    }

What does the seed method do?

fetchCount checks if data already exists. The guard statement exits early so you only seed once.

The loop creates 10 bookings with different guests, properties, and statuses. Each booking's check-in is offset by one day (86400 seconds) so they sort visibly by date.

  • Save SceneDelegate.swift (Cmd+S).

Before you run the app, what do you expect to see in the Simulator?

  • Press Cmd+R to build and run.

The app displays a list of 10 bookings. Each row shows a guest name, property name, check-in date, and a colored status label.

Seeing an empty table or a crash?

  • If the table is empty, confirm seedDataIfNeeded is called before creating the BookingsViewController. The seed must run first so data exists when loadBookings() fetches.
  • If you see a crash mentioning ModelContainer, ensure Booking.swift has the @Model macro and is part of the BookingDashboard target.
  • If you previously ran the app with the placeholder VC, reset the Simulator (Device > Erase All Content and Settings) to clear any stale SwiftData store.

My booking list is empty even though I have the seed method. Help me debug.

✔️ Awesome, I've got everything!

Great. Double check you have saved all four files: Booking.swift, BookingItem.swift, BookingsViewController.swift, and SceneDelegate.swift.

ⓧ I'd like to double check the full code

Scroll up and confirm each code block is in place, in order:

  • Booking.swift contains the @Model class with five properties and an initializer.
  • BookingItem.swift contains the Hashable struct with a nested BookingStatus enum that has displayName, displayColor, and sortPriority computed properties.
  • BookingsViewController.swift has the class with Section enum, dataSource lazy var, init(modelContext:), viewDidLoad, setupUI, loadBookings, and applySnapshot.
  • SceneDelegate.swift creates a ModelContainer, calls seedDataIfNeeded, and injects the context into BookingsViewController.

Your booking dashboard is alive. Next up, you will introduce a fat protocol that handles all data operations. Then you will feel the pain it causes when testing, setting the stage for Interface Segregation.

Build the Fat Protocol and Hit the Wall

Your booking list works, but all the data-fetching logic lives directly inside the ViewController. That tight coupling makes the code impossible to test in isolation.

In this step, you will extract that logic into a protocol-based service layer. You will build a single "fat" protocol that handles everything: fetching, sorting, filtering, and updating. Then you will try to write a unit test and discover firsthand why bundling all those responsibilities into one protocol creates a painful testing experience.

In this step, get ready to:
  • Define a BookingManagerProtocol with four methods and a concrete BookingManager class.
  • Wire the manager into the ViewController so all logic flows through the protocol.
  • Write a test mock and observe the ISP violation forcing you to implement unused methods.
Define the fat protocol and concrete manager

A protocol defines the contract that any booking service must fulfill. Right now, you will put all four responsibilities into a single protocol to see what happens when the contract grows too large.

  • In Xcode, right-click the BookingDashboard group in the Project Navigator.
  • Select New File..., choose Swift File, and name it BookingProtocols.swift.
  • Replace the file contents with this protocol definition:
import Foundation

// Defines the available sort orders for bookings
enum SortOrder {
    case date, name, status
}

// A single protocol bundling ALL booking operations
protocol BookingManagerProtocol {
    func fetchBookings() async throws -> [BookingItem]
    func sortBookings(_ bookings: [BookingItem], by order: SortOrder) -> [BookingItem]
    func filterBookings(_ bookings: [BookingItem], status: String?) -> [BookingItem]
    func updateStatus(id: UUID, newStatus: String) async throws
}

What does this code do?

The SortOrder enum gives you a type-safe way to specify how bookings should be sorted.

The BookingManagerProtocol bundles four distinct responsibilities into one contract: fetching from persistence, sorting in-memory, filtering by status, and updating a booking's state. Any type conforming to this protocol must implement all four methods.

  • Save BookingProtocols.swift.
  • Create another new Swift file in the same group and name it BookingManager.swift.
  • Add the class declaration and the fetchBookings() implementation by pasting this code:
import Foundation
import SwiftData

@MainActor
final class BookingManager: BookingManagerProtocol {
    private let modelContext: ModelContext

    init(modelContext: ModelContext) {
        self.modelContext = modelContext
    }

    // Fetches all Booking models and maps them to BookingItem structs
    func fetchBookings() async throws -> [BookingItem] {
        let descriptor = FetchDescriptor<Booking>()
        let bookings = try modelContext.fetch(descriptor)
        return bookings.map { booking in
            BookingItem(
                id: booking.id,
                guestName: booking.guestName,
                propertyName: booking.propertyName,
                checkIn: booking.checkIn,
                status: BookingItem.BookingStatus(rawValue: booking.status) ?? .pending
            )
        }
    }
}

What does this code do?

The @MainActor annotation ensures the class runs on the main thread, which is required for ModelContext access in Swift 6 strict concurrency.

The fetchBookings() method uses a FetchDescriptor to query all Booking models from SwiftData, then maps them into the Hashable BookingItem structs your diffable data source needs.

  • Save the file and press Cmd+B to build.
  • The build succeeds because the new files do not conflict with the existing code.

Build failed?

Make sure both new files are added to the BookingDashboard target. Select the file in the Project Navigator, open the File Inspector (right panel), and confirm the target membership checkbox is checked.

Check that BookingProtocols.swift imports Foundation and BookingManager.swift imports both Foundation and SwiftData.

Help me fix a build error in my BookingManager or BookingProtocols file.

  • Now add the remaining three methods below the closing brace of fetchBookings() but still inside the class:
    // Sorts bookings by the given order
    func sortBookings(_ bookings: [BookingItem], by order: SortOrder) -> [BookingItem] {
        switch order {
        case .date: return bookings.sorted { $0.checkIn < $1.checkIn }
        case .name: return bookings.sorted { $0.guestName.localizedCaseInsensitiveCompare($1.guestName) == .orderedAscending }
        case .status: return bookings.sorted { $0.status.sortPriority < $1.status.sortPriority }
        }
    }

    // Filters bookings to only those matching a given status
    func filterBookings(_ bookings: [BookingItem], status: String?) -> [BookingItem] {
        guard let status else { return bookings }
        return bookings.filter { $0.status.rawValue == status }
    }

    // Persists a status change back to SwiftData
    func updateStatus(id: UUID, newStatus: String) async throws {
        let descriptor = FetchDescriptor<Booking>(predicate: #Predicate { $0.id == id })
        guard let booking = try modelContext.fetch(descriptor).first else { return }
        booking.status = newStatus
        try modelContext.save()
    }

What does this code do?

  • sortBookings(_:by:) uses a switch on SortOrder to sort by date, guest name, or status priority.
  • filterBookings(_:status:) returns only bookings matching a given status string, or all bookings if nil is passed.
  • updateStatus(id:newStatus:) finds a Booking by UUID, changes its status, and saves the context.
  • Save BookingManager.swift and press Cmd+B to build.
  • The build succeeds again. Your concrete manager fully satisfies the protocol contract.
Wire BookingManager into the ViewController

The new manager exists but nothing uses it yet. You need to swap the ViewController's direct ModelContext dependency for the protocol-based service.

  • In BookingsViewController.swift, find this line near the top of the class:
    private let modelContext: ModelContext
  • Replace it with:
    private let service: BookingManagerProtocol
  • Find the initializer:
    init(modelContext: ModelContext) {
        self.modelContext = modelContext
        super.init(nibName: nil, bundle: nil)
    }
  • Replace it with:
    init(service: BookingManagerProtocol) {
        self.service = service
        super.init(nibName: nil, bundle: nil)
    }
  • Find the loadBookings() method:
    private func loadBookings() async {
        let descriptor = FetchDescriptor<Booking>()
        do {
            let raw = try modelContext.fetch(descriptor)
            bookings = raw.map { BookingItem(id: $0.id, guestName: $0.guestName, propertyName: $0.propertyName, checkIn: $0.checkIn, status: BookingItem.BookingStatus(rawValue: $0.status) ?? .pending) }
            applySnapshot()
        } catch {
            print("Failed to load bookings: \(error)")
        }
    }
  • Replace it with this simplified version that delegates to the service:
    private func loadBookings() async {
        do {
            bookings = try await service.fetchBookings()
            applySnapshot()
        } catch {
            print("Failed to load bookings: \(error)")
        }
    }

Why is this better?

The ViewController no longer knows about FetchDescriptor, ModelContext, or how Booking models map to BookingItem structs. It just asks the service for bookings and displays them. This separation is Dependency Inversion in action: the high-level module depends on an abstraction, not a concrete implementation.

  • Save BookingsViewController.swift.
  • Now open SceneDelegate.swift and find these two lines inside scene(_:willConnectTo:options:):
        let viewController = BookingsViewController(modelContext: context)
  • Replace that single line with these two lines:
        let manager = BookingManager(modelContext: context)
        let viewController = BookingsViewController(service: manager)
  • Save SceneDelegate.swift.
  • Press Cmd+R to build and run in the Simulator.
  • The app displays the same 10 bookings with guest names, properties, dates, and colored status labels. The behavior is identical to before.

App crashes on launch?

Verify that the import SwiftData line is still present in SceneDelegate.swift. The BookingManager init requires a ModelContext which comes from the container created on the line above.

Also check that you removed the old import SwiftData from BookingsViewController.swift if it was there, since the VC no longer uses SwiftData types directly.

Help me debug why my BookingDashboard crashes after wiring in BookingManager.

Hit the wall with a test mock

The refactoring looks clean. All logic flows through a protocol, which should make testing easy. But try writing a mock that only tests the sorting logic and see what happens.

  • In the Project Navigator, expand the BookingDashboardTests group.
  • Right-click it, select New File..., choose Swift File, and name it BookingManagerTests.swift. Make sure the target is set to BookingDashboardTests.
  • Replace the file contents with this test mock:
import XCTest
@testable import BookingDashboard

final class MockBookingManager: BookingManagerProtocol {
    // ISP violation: forced to implement methods I do not use.
    func fetchBookings() async throws -> [BookingItem] { return [] }
    func sortBookings(_ bookings: [BookingItem], by order: SortOrder) -> [BookingItem] { return bookings }
    func filterBookings(_ bookings: [BookingItem], status: String?) -> [BookingItem] { return bookings }
    func updateStatus(id: UUID, newStatus: String) async throws {}
}

final class BookingManagerTests: XCTestCase {
    func testSortByName() {
        let mock = MockBookingManager()
        // Must implement all 4 methods just to test sorting
    }
}

What does this code do?

MockBookingManager conforms to BookingManagerProtocol. The compiler forces it to implement all four methods even though this test only cares about sorting.

The three extra stub methods (fetchBookings, filterBookings, updateStatus) return empty or pass-through values. They exist only to satisfy the compiler. This is the cost of a fat protocol.

  • Save BookingManagerTests.swift.

Before you build, think about this: you only want to test sorting. How many methods did the compiler force you to write in that mock?

  • Press Cmd+B to build the test target.

The build succeeds, but look at your mock. You wrote four method stubs to test one thing. That is the Interface Segregation Principle violation in action: consumers are forced to depend on methods they do not use.

Why is this a problem in interviews?

When Jens asks you to demonstrate testable architecture, a mock with three dead stub methods signals tight coupling. It shows the protocol was designed around the implementation, not around what consumers actually need.

In the next step, you will split this fat protocol into focused, single-responsibility protocols. Your test mock will shrink to exactly one method.

✔️ Awesome, I've got everything!

Great. Double check you have saved all files: BookingProtocols.swift, BookingManager.swift, BookingsViewController.swift, SceneDelegate.swift, and BookingManagerTests.swift.

ⓧ I'd like to double check the full code

Here is BookingProtocols.swift:

import Foundation

enum SortOrder {
    case date, name, status
}

protocol BookingManagerProtocol {
    func fetchBookings() async throws -> [BookingItem]
    func sortBookings(_ bookings: [BookingItem], by order: SortOrder) -> [BookingItem]
    func filterBookings(_ bookings: [BookingItem], status: String?) -> [BookingItem]
    func updateStatus(id: UUID, newStatus: String) async throws
}

Here is BookingManager.swift:

import Foundation
import SwiftData

@MainActor
final class BookingManager: BookingManagerProtocol {
    private let modelContext: ModelContext

    init(modelContext: ModelContext) {
        self.modelContext = modelContext
    }

    func fetchBookings() async throws -> [BookingItem] {
        let descriptor = FetchDescriptor<Booking>()
        let bookings = try modelContext.fetch(descriptor)
        return bookings.map { booking in
            BookingItem(
                id: booking.id,
                guestName: booking.guestName,
                propertyName: booking.propertyName,
                checkIn: booking.checkIn,
                status: BookingItem.BookingStatus(rawValue: booking.status) ?? .pending
            )
        }
    }
    func sortBookings(_ bookings: [BookingItem], by order: SortOrder) -> [BookingItem] {
        switch order {
        case .date: return bookings.sorted { $0.checkIn < $1.checkIn }
        case .name: return bookings.sorted { $0.guestName.localizedCaseInsensitiveCompare($1.guestName) == .orderedAscending }
        case .status: return bookings.sorted { $0.status.sortPriority < $1.status.sortPriority }
        }
    }

    func filterBookings(_ bookings: [BookingItem], status: String?) -> [BookingItem] {
        guard let status else { return bookings }
        return bookings.filter { $0.status.rawValue == status }
    }

    func updateStatus(id: UUID, newStatus: String) async throws {
        let descriptor = FetchDescriptor<Booking>(predicate: #Predicate { $0.id == id })
        guard let booking = try modelContext.fetch(descriptor).first else { return }
        booking.status = newStatus
        try modelContext.save()
    }
}

Here is BookingsViewController.swift:

import UIKit

final class BookingsViewController: UIViewController {
    enum Section { case main }

    private let service: BookingManagerProtocol
    private var bookings: [BookingItem] = []

    private let tableView = UITableView(frame: .zero, style: .plain)
    private lazy var dataSource: UITableViewDiffableDataSource<Section, BookingItem> = {
        UITableViewDiffableDataSource<Section, BookingItem>(tableView: tableView) { tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "BookingCell", for: indexPath)
            let formatter = DateFormatter()
            formatter.dateStyle = .medium
            formatter.timeStyle = .none
            var content = cell.defaultContentConfiguration()
            content.text = "\(item.guestName) - \(item.propertyName)"
            content.secondaryText = "\(formatter.string(from: item.checkIn)) | \(item.status.displayName)"
            content.secondaryTextProperties.color = item.status.displayColor
            cell.contentConfiguration = content
            return cell
        }
    }()
    init(service: BookingManagerProtocol) {
        self.service = service
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Bookings"
        view.backgroundColor = .systemBackground
        setupUI()
        Task { await loadBookings() }
    }

    private func setupUI() {
        tableView.frame = view.bounds
        tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "BookingCell")
        view.addSubview(tableView)
    }

    private func loadBookings() async {
        do {
            bookings = try await service.fetchBookings()
            applySnapshot()
        } catch {
            print("Failed to load bookings: \(error)")
        }
    }

    private func applySnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, BookingItem>()
        snapshot.appendSections([.main])
        snapshot.appendItems(bookings)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

Here is the updated SceneDelegate.swift (only the scene(_:willConnectTo:options:) method changed):

import UIKit
import SwiftData

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)

        let container = try! ModelContainer(for: Booking.self)
        let context = container.mainContext
        seedDataIfNeeded(context: context)

        let manager = BookingManager(modelContext: context)
        let viewController = BookingsViewController(service: manager)
        window?.rootViewController = UINavigationController(rootViewController: viewController)
        window?.makeKeyAndVisible()
    }
    private func seedDataIfNeeded(context: ModelContext) {
        let descriptor = FetchDescriptor<Booking>()
        let count = (try? context.fetchCount(descriptor)) ?? 0
        guard count == 0 else { return }

        let guests = ["Alice Johnson", "Bob Smith", "Carol White", "David Brown", "Eva Martinez",
                      "Frank Lee", "Grace Kim", "Henry Wilson", "Iris Chen", "Jack Davis"]
        let properties = ["Grand Hotel Downtown", "Seaside Resort", "Mountain Lodge",
                          "City Center Inn", "Lakefront Suites"]
        let statuses = ["confirmed", "checked_in", "checked_out", "cancelled", "pending"]

        for i in 0..<10 {
            let booking = Booking(
                guestName: guests[i],
                propertyName: properties[i % properties.count],
                checkIn: Date().addingTimeInterval(Double(i) * 86400),
                status: statuses[i % statuses.count]
            )
            context.insert(booking)
        }
        try? context.save()
    }
}

Here is BookingManagerTests.swift:

import XCTest
@testable import BookingDashboard

final class MockBookingManager: BookingManagerProtocol {
    // ISP violation: forced to implement methods I do not use.
    func fetchBookings() async throws -> [BookingItem] { return [] }
    func sortBookings(_ bookings: [BookingItem], by order: SortOrder) -> [BookingItem] { return bookings }
    func filterBookings(_ bookings: [BookingItem], status: String?) -> [BookingItem] { return bookings }
    func updateStatus(id: UUID, newStatus: String) async throws {}
}

final class BookingManagerTests: XCTestCase {
    func testSortByName() {
        let mock = MockBookingManager()
        // Must implement all 4 methods just to test sorting
    }
}

You have successfully routed all booking logic through a protocol. The app works identically, but your test file reveals the cost: a fat protocol forces every consumer to carry dead weight. In the next step, you will split this monolith into focused protocols that eliminate the pain.

Apply Interface Segregation and Dependency Inversion

In the last step, you felt the pain of the fat protocol firsthand. Your MockBookingManager had to implement all four methods just to test sorting. That is wasted code and a clear signal that the protocol is doing too much.

The Interface Segregation Principle says: split protocols so consumers depend only on what they actually use. The Dependency Inversion Principle says: depend on abstractions, not concrete classes. Together, they let you write a mock with exactly one method and prove your architecture is testable.

In this step, get ready to:
  • Split the fat protocol into four focused protocols and recombine them with protocol composition.
  • Narrow the ViewController's dependency to only the protocols it uses.
  • Write a focused unit test with a one-method mock that passes cleanly.
Split the fat protocol into focused protocols

Before you can split BookingManagerProtocol, you need the BookingSortStrategy protocol that the new BookingSortable will reference. This protocol defines a single sort method that any sort algorithm must implement.

  • Create a new Swift file called BookingSortStrategy.swift in your BookingDashboard group by right-clicking the group in the Project Navigator and selecting New File, then Swift File.
  • Replace the file contents with the sort strategy protocol and two concrete strategies:
import Foundation

protocol BookingSortStrategy {
    var name: String { get }
    func sort(_ bookings: [BookingItem]) -> [BookingItem]
}

struct DateSortStrategy: BookingSortStrategy {
    let name = "DateSortStrategy"
    func sort(_ bookings: [BookingItem]) -> [BookingItem] {
        bookings.sorted { $0.checkIn < $1.checkIn }
    }
}

struct NameSortStrategy: BookingSortStrategy {
    let name = "NameSortStrategy"
    func sort(_ bookings: [BookingItem]) -> [BookingItem] {
        bookings.sorted { $0.guestName.localizedCaseInsensitiveCompare($1.guestName) == .orderedAscending }
    }
}

What does this code do?

The BookingSortStrategy protocol defines a contract: any sort algorithm must provide a name (for logging later) and a sort function that takes bookings in and returns them reordered.

DateSortStrategy sorts by check-in date (earliest first). NameSortStrategy sorts alphabetically by guest name. Each is a standalone type you can swap in at runtime.

  • Confirm BookingSortStrategy.swift appears in the Project Navigator under your BookingDashboard group.

Now replace the fat protocol with four focused protocols. Each one describes a single capability.

  • Open BookingProtocols.swift and select all the existing code.
  • Replace the entire file contents with these focused protocols:
import Foundation

protocol BookingFetchable {
    func fetchBookings() async throws -> [BookingItem]
}

protocol BookingSortable {
    func sortBookings(_ bookings: [BookingItem], using strategy: BookingSortStrategy) -> [BookingItem]
}

protocol BookingFilterable {
    func filterBookings(_ bookings: [BookingItem], status: BookingItem.BookingStatus?) -> [BookingItem]
}

protocol BookingUpdatable {
    func updateStatus(id: UUID, newStatus: String) async throws
}

typealias FullBookingManager = BookingFetchable & BookingSortable & BookingFilterable & BookingUpdatable

What does this code do?

Each protocol now holds exactly one responsibility. BookingFetchable fetches. BookingSortable sorts. BookingFilterable filters. BookingUpdatable mutates status.

The typealias FullBookingManager uses protocol composition (&) to recombine all four into a single type constraint. Any class that conforms to FullBookingManager must implement all four protocols, but consumers can depend on whichever subset they need.

Your project will show compiler errors because BookingManager still references the old protocol. Update it now.

  • Open BookingManager.swift and find the class declaration line:
final class BookingManager: BookingManagerProtocol {
  • Replace BookingManagerProtocol with FullBookingManager, so the line reads:
final class BookingManager: FullBookingManager {
  • Find the existing sortBookings method:
func sortBookings(_ bookings: [BookingItem], by order: SortOrder) -> [BookingItem] {
    switch order {
    case .date: return bookings.sorted { $0.checkIn < $1.checkIn }
    case .name: return bookings.sorted { $0.guestName.localizedCaseInsensitiveCompare($1.guestName) == .orderedAscending }
    case .status: return bookings.sorted { $0.status.sortPriority < $1.status.sortPriority }
    }
}
  • Replace the entire method with this Strategy-based version:
func sortBookings(_ bookings: [BookingItem], using strategy: BookingSortStrategy) -> [BookingItem] {
    strategy.sort(bookings)
}
  • Find the filterBookings method signature:
func filterBookings(_ bookings: [BookingItem], status: String?) -> [BookingItem] {
    guard let status else { return bookings }
    return bookings.filter { $0.status.rawValue == status }
}
  • Replace it with the typed version that uses BookingItem.BookingStatus? instead of String?:
func filterBookings(_ bookings: [BookingItem], status: BookingItem.BookingStatus?) -> [BookingItem] {
    guard let status else { return bookings }
    return bookings.filter { $0.status == status }
}
  • Save BookingManager.swift.
  • Press Cmd+B to build. You should see Build Succeeded in the Xcode activity area.

Seeing compiler errors?

  • If you see an error about SortOrder still being referenced, delete the old enum SortOrder from BookingProtocols.swift. It is no longer needed.
  • If BookingSortStrategy is not found, confirm your new BookingSortStrategy.swift file has the BookingDashboard target checked in the File Inspector (right panel).

Help me fix my build errors after splitting protocols.

Narrow the ViewController's dependency

The ViewController only fetches and sorts bookings. It never filters or updates status. Narrowing its dependency type to BookingFetchable & BookingSortable makes this explicit and prevents accidental coupling.

  • Open BookingsViewController.swift and find the service property:
private let service: BookingManagerProtocol
  • Replace it with these two lines (the narrowed type and a strategy property):
private let service: BookingFetchable & BookingSortable
private var bookings: [BookingItem] = []
private var currentStrategy: BookingSortStrategy = DateSortStrategy()
  • Find the initializer:
init(service: BookingManagerProtocol) {
  • Replace it with the narrowed type:
init(service: BookingFetchable & BookingSortable) {
  • Find the loadBookings method:
private func loadBookings() async {
    do {
        bookings = try await service.fetchBookings()
        applySnapshot()
    } catch {
        print("Failed to load bookings: \(error)")
    }
}
  • Replace it with a version that sorts through the strategy:
private func loadBookings() async {
    do {
        let fetched = try await service.fetchBookings()
        bookings = service.sortBookings(fetched, using: currentStrategy)
        applySnapshot()
    } catch {
        print("Failed to load bookings: \(error)")
    }
}

Why narrow the dependency?

By declaring service: BookingFetchable & BookingSortable, the ViewController communicates exactly which capabilities it needs. A future developer reading this code instantly knows it only fetches and sorts. It cannot accidentally call updateStatus or filterBookings.

This is Dependency Inversion in action. The high-level module (ViewController) depends on abstractions (protocols), not on the concrete BookingManager class.

  • Save BookingsViewController.swift.
  • Press Cmd+R to build and run in the Simulator.

The app should display the same booking list as before. The refactoring changed the internal architecture without altering the user-facing behavior.

App crashes or shows a blank list?

  • Check that SceneDelegate.swift still passes manager to BookingsViewController(service: manager). BookingManager conforms to FullBookingManager, which includes BookingFetchable & BookingSortable, so it still satisfies the initializer.
  • If the list is empty, reset the Simulator data (Device > Erase All Content and Settings) to re-trigger seeding.

Help me fix my ViewController after narrowing the protocol dependency.

Write a focused unit test that proves ISP works

The whole point of splitting protocols was testability. Your mock should now conform to a single protocol with exactly one method.

  • Open BookingManagerTests.swift and select all the existing code.
  • Replace the entire file with this focused test:
import XCTest
@testable import BookingDashboard

final class MockSortService: BookingSortable {
    func sortBookings(_ bookings: [BookingItem], using strategy: BookingSortStrategy) -> [BookingItem] {
        strategy.sort(bookings)
    }
}

final class BookingManagerTests: XCTestCase {
    func testSortByNameReturnsAlphabeticalOrder() {
        let mock = MockSortService()
        let bookings = [
            BookingItem(id: UUID(), guestName: "Zara", propertyName: "Hotel A", checkIn: Date(), status: .confirmed),
            BookingItem(id: UUID(), guestName: "Alice", propertyName: "Hotel B", checkIn: Date(), status: .pending),
            BookingItem(id: UUID(), guestName: "Mike", propertyName: "Hotel C", checkIn: Date(), status: .checkedIn)
        ]
        let sorted = mock.sortBookings(bookings, using: NameSortStrategy())
        XCTAssertEqual(sorted[0].guestName, "Alice")
        XCTAssertEqual(sorted[1].guestName, "Mike")
        XCTAssertEqual(sorted[2].guestName, "Zara")
    }
}

What does this test prove?

MockSortService conforms to BookingSortable only. It has exactly one method. No stubs for fetch, filter, or update. That is the ISP payoff: your mock matches the scope of what you are testing.

The test creates three bookings out of alphabetical order, passes them through NameSortStrategy, and asserts the result is alphabetical. If this passes, your Strategy and ISP wiring are correct.

  • Save BookingManagerTests.swift.

Before you run the tests, what do you expect to happen? Will all tests pass, or will something fail?

  • Press Cmd+U to run all tests.

You should see a green checkmark next to testSortByNameReturnsAlphabeticalOrder in the Test Navigator. All tests pass.

Test fails or won't compile?

  • If NameSortStrategy is not found, make sure BookingSortStrategy.swift is included in both the main target and the test target. Check the file's Target Membership in the File Inspector.
  • If BookingSortable is not found, confirm BookingProtocols.swift is included in the test target's Target Membership.
  • If assertions fail, double-check that NameSortStrategy uses localizedCaseInsensitiveCompare for proper alphabetical ordering.

Help me fix my unit test for the Strategy pattern.

✔️ Awesome, I've got everything!

Great. Double-check you have saved all files before moving on.

ⓧ I'd like to double check the full code

Here are the key files as they should look at the end of this step. Scroll up and confirm each one matches.

BookingSortStrategy.swift:

import Foundation

protocol BookingSortStrategy {
    var name: String { get }
    func sort(_ bookings: [BookingItem]) -> [BookingItem]
}

struct DateSortStrategy: BookingSortStrategy {
    let name = "DateSortStrategy"
    func sort(_ bookings: [BookingItem]) -> [BookingItem] {
        bookings.sorted { $0.checkIn < $1.checkIn }
    }
}

struct NameSortStrategy: BookingSortStrategy {
    let name = "NameSortStrategy"
    func sort(_ bookings: [BookingItem]) -> [BookingItem] {
        bookings.sorted { $0.guestName.localizedCaseInsensitiveCompare($1.guestName) == .orderedAscending }
    }
}

BookingProtocols.swift:

import Foundation

protocol BookingFetchable {
    func fetchBookings() async throws -> [BookingItem]
}

protocol BookingSortable {
    func sortBookings(_ bookings: [BookingItem], using strategy: BookingSortStrategy) -> [BookingItem]
}

protocol BookingFilterable {
    func filterBookings(_ bookings: [BookingItem], status: BookingItem.BookingStatus?) -> [BookingItem]
}

protocol BookingUpdatable {
    func updateStatus(id: UUID, newStatus: String) async throws
}

typealias FullBookingManager = BookingFetchable & BookingSortable & BookingFilterable & BookingUpdatable

BookingManagerTests.swift:

import XCTest
@testable import BookingDashboard

final class MockSortService: BookingSortable {
    func sortBookings(_ bookings: [BookingItem], using strategy: BookingSortStrategy) -> [BookingItem] {
        strategy.sort(bookings)
    }
}

final class BookingManagerTests: XCTestCase {
    func testSortByNameReturnsAlphabeticalOrder() {
        let mock = MockSortService()
        let bookings = [
            BookingItem(id: UUID(), guestName: "Zara", propertyName: "Hotel A", checkIn: Date(), status: .confirmed),
            BookingItem(id: UUID(), guestName: "Alice", propertyName: "Hotel B", checkIn: Date(), status: .pending),
            BookingItem(id: UUID(), guestName: "Mike", propertyName: "Hotel C", checkIn: Date(), status: .checkedIn)
        ]
        let sorted = mock.sortBookings(bookings, using: NameSortStrategy())
        XCTAssertEqual(sorted[0].guestName, "Alice")
        XCTAssertEqual(sorted[1].guestName, "Mike")
        XCTAssertEqual(sorted[2].guestName, "Zara")
    }
}

You now have a protocol architecture where each concern lives behind its own boundary. Your ViewController depends only on what it uses, and your test proves sorting works with a single-method mock. Next up, you will add more sort strategies and a segmented control to switch between them at runtime.

Add the Strategy Pattern for Swappable Sort Algorithms

Your architecture now delegates sorting to a BookingSortStrategy protocol, but only one strategy exists. When the product team requests sorting by guest name or by status urgency, you would need to modify BookingManager each time.

The Strategy pattern solves this by letting you add new sort algorithms as new types, without changing existing code. In this step, you will create two additional strategies and wire a segmented control to swap between them at runtime.

In this step, get ready to:
  • Add NameSortStrategy and StatusPrioritySortStrategy as new conforming types.
  • Wire a UISegmentedControl to swap sort strategies at runtime.
  • Verify animated row reordering when switching segments.
Add NameSortStrategy and StatusPrioritySortStrategy

Each new sort algorithm is its own struct conforming to BookingSortStrategy. You add behavior by writing new types, not by editing existing ones.

  • In BookingSortStrategy.swift, add the following struct below DateSortStrategy:
struct NameSortStrategy: BookingSortStrategy {
    let name = "NameSortStrategy"
    func sort(_ bookings: [BookingItem]) -> [BookingItem] {
        // Sort alphabetically by guest name, case-insensitive
        bookings.sorted { $0.guestName.localizedCaseInsensitiveCompare($1.guestName) == .orderedAscending }
    }
}

What does this code do?

NameSortStrategy uses localizedCaseInsensitiveCompare to sort guest names alphabetically regardless of capitalization. This handles international characters correctly, unlike a raw < comparison on strings.

  • Save BookingSortStrategy.swift.
  • Run your tests with Cmd+U. The existing testSortByNameReturnsAlphabeticalOrder test should pass, confirming your new strategy works.

Test not passing?

Make sure the struct name is exactly NameSortStrategy with a capital N. The test file references this type by name.

Check that your struct conforms to BookingSortStrategy and has both the name property and the sort method.

Why is my NameSortStrategy not conforming to BookingSortStrategy?

  • Now add the status priority strategy below NameSortStrategy in the same file:
struct StatusPrioritySortStrategy: BookingSortStrategy {
    let name = "StatusPrioritySortStrategy"
    func sort(_ bookings: [BookingItem]) -> [BookingItem] {
        // Sort by urgency: checkedIn first, cancelled last
        bookings.sorted { $0.status.sortPriority < $1.status.sortPriority }
    }
}

What does this code do?

StatusPrioritySortStrategy uses the sortPriority computed property you defined in BookingItem.BookingStatus. Guests currently checked in surface to the top (priority 0), while cancelled bookings sink to the bottom (priority 4).

  • Save BookingSortStrategy.swift.

Why Strategy instead of a switch statement?

You could put all sort logic inside a single switch on an enum case. That works for three options. But each new sort option requires editing that switch, which violates the Open/Closed Principle.

With Strategy, adding a fourth sort (e.g. by property name) means creating one new struct. Zero existing code changes. At scale with 10+ sort options, this pays off significantly.

✔️ Awesome, I've got everything!

Great. Double check you have saved BookingSortStrategy.swift with all three strategy structs.

ⓧ I'd like to double check the full code

import Foundation

protocol BookingSortStrategy {
    var name: String { get }
    func sort(_ bookings: [BookingItem]) -> [BookingItem]
}

struct DateSortStrategy: BookingSortStrategy {
    let name = "DateSortStrategy"
    func sort(_ bookings: [BookingItem]) -> [BookingItem] {
        bookings.sorted { $0.checkIn < $1.checkIn }
    }
}

struct NameSortStrategy: BookingSortStrategy {
    let name = "NameSortStrategy"
    func sort(_ bookings: [BookingItem]) -> [BookingItem] {
        bookings.sorted { $0.guestName.localizedCaseInsensitiveCompare($1.guestName) == .orderedAscending }
    }
}

struct StatusPrioritySortStrategy: BookingSortStrategy {
    let name = "StatusPrioritySortStrategy"
    func sort(_ bookings: [BookingItem]) -> [BookingItem] {
        bookings.sorted { $0.status.sortPriority < $1.status.sortPriority }
    }
}
Wire the UISegmentedControl for runtime strategy swapping

You have three strategies ready. Now the learner (or in production, the partner staff) needs a way to switch between them without restarting the app.

  • In BookingsViewController.swift, add this property declaration below the dataSource lazy var:
private let segmentedControl: UISegmentedControl = {
    let control = UISegmentedControl(items: ["Date", "Name", "Status"])
    control.selectedSegmentIndex = 0
    return control
}()

What does this code do?

This creates a UISegmentedControl with three segments matching your three strategies. The selectedSegmentIndex = 0 starts on "Date" since DateSortStrategy is the default.

  • Find the setupUI() method. Add these two lines at the end of it, after view.addSubview(tableView):
// Connect segment taps to our sort handler
segmentedControl.addTarget(self, action: #selector(sortChanged), for: .valueChanged)
navigationItem.titleView = segmentedControl
  • Now add the sortChanged method that handles segment taps. Place it below the loadBookings() method:
@objc private func sortChanged(_ sender: UISegmentedControl) {
    // Swap the strategy based on which segment is tapped
    switch sender.selectedSegmentIndex {
    case 0: currentStrategy = DateSortStrategy()
    case 1: currentStrategy = NameSortStrategy()
    case 2: currentStrategy = StatusPrioritySortStrategy()
    default: break
    }
    // Re-sort with the new strategy and animate the change
    bookings = service.sortBookings(bookings, using: currentStrategy)
    applySnapshot()
}

What does this code do?

  • The switch maps each segment index to a fresh strategy instance. Tapping "Name" creates a new NameSortStrategy() and assigns it to currentStrategy.
  • The re-sort delegates to service.sortBookings, which passes the work to the strategy. The controller never knows HOW sorting happens.
  • Calling applySnapshot() with animatingDifferences: true triggers the diffable data source to animate rows into their new positions.

Before you run, what do you think will happen when you tap the "Name" segment? Will the rows jump instantly or animate smoothly?

  • Save BookingsViewController.swift.
  • Run the app with Cmd+R.
  • Tap the Name segment. The rows animate into alphabetical order (Alice Johnson at the top).
  • Tap the Status segment. The rows reorder by urgency, with "Checked In" bookings rising to the top.
  • Tap Date to return to chronological order.

The rows animate smoothly into new positions each time. The diffable data source compares the old and new snapshots by item identity and moves cells rather than reloading them.

Segmented control not visible?

Check that navigationItem.titleView = segmentedControl is inside setupUI(). The segmented control only appears if the view controller is wrapped in a UINavigationController, which your SceneDelegate already provides.

If tapping segments does nothing, verify the addTarget line uses #selector(sortChanged) and the method is marked @objc.

Why isn't my UISegmentedControl responding to taps in BookingsViewController?

What about BookingManager?

Open BookingManager.swift and confirm you made zero changes to it. You added three sort algorithms and a UI control without modifying the data layer. That is the Open/Closed Principle in action: open for extension, closed for modification.

✔️ Awesome, I've got everything!

Great. Confirm you have saved BookingsViewController.swift and that tapping each segment triggers an animated reorder.

ⓧ I'd like to double check the full code

Scroll up and confirm your BookingsViewController.swift has all of these pieces, in order:

  • The segmentedControl property declaration (below dataSource).
  • The two lines inside setupUI(): addTarget and navigationItem.titleView.
  • The sortChanged(_:) method with the switch statement mapping segments to strategies.

You now have three swappable sort algorithms and a UI to switch between them at runtime. Notice you achieved this without touching BookingManager. Next up, you will add a Decorator that wraps the data-fetching layer to add logging, proving OCP from a completely different angle.

Add the Decorator Pattern for Cross-Cutting Concerns

Your Strategy pattern lets you add sort algorithms without modifying BookingManager. That proves the Open/Closed Principle from the consumer side.

Now imagine you need logging to debug sort operations in production. You could add print statements inside BookingManager, but that pollutes business logic with infrastructure concerns and violates the Single Responsibility Principle. The Decorator Pattern solves this by wrapping an existing protocol conformance to add behavior without touching the original class.

In this step, get ready to:
  • Create a LoggingBookingDecorator that wraps any BookingFetchable & BookingSortable and logs every operation.
  • Wire the decorator into your DI setup so the ViewController never knows it exists.
  • Verify timestamped log lines appear in the Xcode console for every fetch and sort.
Create the LoggingBookingDecorator

The Decorator conforms to the same protocols as the object it wraps. Callers interact with it identically. The decorator intercepts each call, logs it, and delegates to the wrapped instance.

  • Create a new Swift file in your BookingDashboard group called LoggingBookingDecorator.swift.
  • Add the following code:
import Foundation

final class LoggingBookingDecorator: BookingFetchable, BookingSortable {
    // Hold a reference to the real implementation
    private let wrapped: BookingFetchable & BookingSortable
    var logMessages: [String] = []

    init(wrapping: BookingFetchable & BookingSortable) {
        self.wrapped = wrapping
    }

    func fetchBookings() async throws -> [BookingItem] {
        let result = try await wrapped.fetchBookings()
        let message = "[BookingLog] Fetched \(result.count) bookings"
        logMessages.append(message)
        print(message)
        return result
    }

    func sortBookings(_ bookings: [BookingItem], using strategy: BookingSortStrategy) -> [BookingItem] {
        let result = wrapped.sortBookings(bookings, using: strategy)
        let message = "[BookingLog] Sorted by: \(strategy.name)"
        logMessages.append(message)
        print(message)
        return result
    }
}

What does this code do?

This class implements the Decorator pattern. It conforms to the same two protocols that BookingManager does, so any caller expecting BookingFetchable & BookingSortable can receive the decorator instead.

  • The wrapped property stores the real implementation that does the actual work.
  • Each method delegates to wrapped, but adds a log line before returning. The log includes a [BookingLog] prefix so you can filter console output.
  • The logMessages array stores log entries in memory. This makes the decorator testable (you will use this in the Secret Mission).
  • Save LoggingBookingDecorator.swift.
  • Press Cmd+B to build. The project should compile with no errors.

Build failed?

  • Check that your file imports Foundation. The class uses BookingFetchable and BookingSortable from your BookingProtocols.swift file.
  • Confirm the method signatures match the protocols exactly. The fetchBookings() method must be async throws.
  • Make sure the file is added to the BookingDashboard target (check the file inspector on the right).
Wire the decorator into SceneDelegate

The decorator exists but nothing uses it yet. You need to wrap BookingManager with LoggingBookingDecorator before injecting it into the view controller. The key insight: BookingsViewController will not change at all.

  • In SceneDelegate.swift, find these two lines inside the scene(_:willConnectTo:options:) method:
let manager = BookingManager(modelContext: context)
let viewController = BookingsViewController(service: manager)
  • Replace those two lines with these three lines:
let manager = BookingManager(modelContext: context)
let loggingManager = LoggingBookingDecorator(wrapping: manager)
let viewController = BookingsViewController(service: loggingManager)

What changed?

You inserted one new line. Instead of passing manager directly to the view controller, you wrap it with LoggingBookingDecorator first.

The view controller still receives a BookingFetchable & BookingSortable. It has no idea logging was added. This is the power of Dependency Inversion: the consumer depends on the abstraction, so you can swap implementations at the composition root without touching downstream code.

  • Save SceneDelegate.swift.
Verify log output in the console

Before you run the app, think about what you expect to see in the Xcode console. The decorator wraps every fetch and sort call. What should appear when the app launches and loads its initial booking list?

  • Press Cmd+R to build and run in the Simulator.
  • Once the booking list appears, check the Xcode console at the bottom of the screen.

You should see two log lines printed on launch:

[BookingLog] Fetched 10 bookings
[BookingLog] Sorted by: DateSortStrategy
  • Now tap the Name segment in the segmented control.
  • Check the console again.

A new log line appears:

[BookingLog] Sorted by: NameSortStrategy
  • Tap Status and confirm you see [BookingLog] Sorted by: StatusPrioritySortStrategy in the console.

The app behavior is identical to before. The table still animates when you switch sort segments. No code inside BookingManager was touched.

Not seeing log lines?

  • Make sure you pass loggingManager (not manager) to BookingsViewController(service:). If you pass the unwrapped manager, the decorator is never called.
  • Confirm the Xcode console is visible. Click View in the menu bar, then Debug Area, then Activate Console.
  • If the app crashes on launch, check that LoggingBookingDecorator is in the same target as the rest of your source files.

Why use a Decorator instead of modifying BookingManager?

The Decorator adds a layer of indirection, which has a cost: an extra object allocation and an extra method hop. So when is it worth it?

When the cross-cutting concern (logging, caching, analytics) would otherwise be copy-pasted into every implementation. If you later add a CachingBookingDecorator, you can stack it on top of the logging decorator without changing either one. Each decorator has a single responsibility, and you compose them at the DI root.

✔️ Awesome, I've got everything!

Great. Double check you have saved both LoggingBookingDecorator.swift and SceneDelegate.swift.

ⓧ I'd like to double check the full code

Here is the complete LoggingBookingDecorator.swift:

import Foundation

final class LoggingBookingDecorator: BookingFetchable, BookingSortable {
    private let wrapped: BookingFetchable & BookingSortable
    var logMessages: [String] = []

    init(wrapping: BookingFetchable & BookingSortable) {
        self.wrapped = wrapping
    }

    func fetchBookings() async throws -> [BookingItem] {
        let result = try await wrapped.fetchBookings()
        let message = "[BookingLog] Fetched \(result.count) bookings"
        logMessages.append(message)
        print(message)
        return result
    }

    func sortBookings(_ bookings: [BookingItem], using strategy: BookingSortStrategy) -> [BookingItem] {
        let result = wrapped.sortBookings(bookings, using: strategy)
        let message = "[BookingLog] Sorted by: \(strategy.name)"
        logMessages.append(message)
        print(message)
        return result
    }
}

And the updated section of SceneDelegate.swift (inside scene(_:willConnectTo:options:)):

let manager = BookingManager(modelContext: context)
let loggingManager = LoggingBookingDecorator(wrapping: manager)
let viewController = BookingsViewController(service: loggingManager)

window?.rootViewController = UINavigationController(rootViewController: viewController)
window?.makeKeyAndVisible()

You now have a fully protocol-oriented architecture with three design patterns working together: ISP for focused abstractions, Strategy for swappable algorithms, and Decorator for cross-cutting concerns. In the Secret Mission, you will prove the entire composition works in complete isolation with a unit test.

Secret mission

Prove It All Works in Isolation

The ultimate proof that your architecture is sound is a unit test that exercises the full fetch-sort-log flow using only protocol-based mocks. Zero UIKit. Zero SwiftData. Can you prove it all composes correctly in complete isolation?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • BookingDashboard Xcode project on your Desktop.
  • SwiftData SQLite store in the iOS Simulator's app container directory.

Keep everything running

No action needed. Choose this if you are still actively building or want to reference this project before your interview with Jens.

  • Your BookingDashboard project stays on your Desktop, ready to open in Xcode.
  • The Simulator's SwiftData store persists between launches so your seeded bookings remain available.

Pause - I'll come back to this later

Shut down running processes to free up memory while keeping your project files intact.

  • Stop the Simulator by pressing Cmd+. in Xcode.
  • Quit Xcode to free up memory.
  • Your project files remain on your Desktop. Open BookingDashboard.xcodeproj when you are ready to resume.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete the Xcode project by running this command in Terminal:
rm -rf ~/Desktop/BookingDashboard
  • Delete the Simulator's SwiftData store by resetting the Simulator. In Xcode's menu bar, select Device then Erase All Content and Settings.

Nice Work!

Nice Work!

Nice work. You just built a production-grade booking dashboard that demonstrates five design patterns you can walk through line-by-line in your interview with Jens.

You've learned how to:

  • Build a SwiftData-backed UIKit booking list using FetchDescriptor and UITableViewDiffableDataSource with animated row updates when sort order changes.
  • Apply Interface Segregation and Dependency Inversion by splitting a fat protocol into focused abstractions composed with &, so each consumer depends only on what it uses.
  • Implement the Strategy Pattern and Decorator Pattern to add swappable sort algorithms and cross-cutting logging without modifying existing code, proving the Open/Closed Principle.
  • Secret Mission: Prove the entire fetch-sort-log architecture is testable in complete isolation using protocol-based mocks and a spy decorator, with zero UIKit or SwiftData dependencies.

Ready to quiz yourself?