SwiftUI Partner Dashboard Refactor

Refactor a SwiftUI app from God View anti-pattern to clean MVVM architecture.

Introduction

30 Second Summary

In coding interviews, you are often handed a messy file and asked: "What would you change?" The ability to spot anti-patterns and articulate a clean fix is what separates a strong candidate from someone who just memorizes syntax.

In this project, you will build a SwiftUI reservation dashboard app that starts deliberately messy, then refactor it step-by-step into clean, testable architecture. By the end, you will have a concrete before/after story ready for your interview.

What You'll Build

You will open the app, tap through a list of hotel reservations with colored status badges, drill into guest details and messages, and walk an interviewer through exactly how you transformed a 200-line mess into clean MVVM architecture.

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

  • A running Partner Reservation Dashboard where you tap through guest bookings with color-coded status badges, check-in/out dates, and amounts.
  • A before/after architecture story you can walk your interviewer through: here is the anti-pattern, here is what I did, and here is why.
  • Passing unit tests that prove your ViewModel works independently of the UI, demonstrating testability and maintainability on the spot.
  • Secret Mission: Add pull-to-refresh with a loading state and error handling using a protocol-based mock service.

Are there any prerequisites?

You need Xcode 26 installed on your Mac. You should also be familiar with Swift basics (variables, functions, structs) and SwiftUI fundamentals (views, modifiers, basic layouts).

Before We Start

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

Create the Xcode Project

Every refactoring story starts with a codebase. You need a fresh SwiftUI project as the canvas for building the deliberately messy "God View" in the next step.

In this step, you'll create a new Xcode project and run it on the iOS Simulator to confirm your environment is ready for the real work.

In this step, get ready to:
  • Create a new SwiftUI project named PartnerDashboard.
  • Run it on the iPhone 16 Pro simulator and confirm the default screen appears.
Create a new SwiftUI project
  • Press Cmd+Space to open Spotlight search.
  • Type Xcode and press Enter to open it.
  • In the menu bar, click File, then New, then Project.
  • Select the iOS tab at the top of the template chooser.
  • Select App and click Next.
  • Enter PartnerDashboard in the Product Name field.
  • Confirm Interface is set to SwiftUI.
  • Confirm Language is set to Swift.
  • Set Testing System to None. You will add a test target manually in a later step.
  • Click Next.
  • Choose your Desktop as the save location, then click Create.

Xcode opens your new project. You should see ContentView.swift in the editor and PartnerDashboardApp.swift in the Project Navigator on the left.

Set the deployment target to iOS 17

The project defaults to the latest iOS version, but you need iOS 17.0 so you can use the @Observable macro while targeting a wider range of devices.

  • In the Project Navigator (left sidebar), click the PartnerDashboard project file (the blue icon at the very top).
  • In the editor area, select the PartnerDashboard target from the target list on the left.
  • Click the General tab at the top.
  • Find the Minimum Deployments section.
  • Click the iOS version value and change it to 17.0.

Why iOS 17.0?

The @Observable macro was introduced in iOS 17. Setting the deployment target to 17.0 ensures the app can use this modern SwiftUI pattern while remaining compatible with a wide range of devices.

Run the project on the simulator
  • In the toolbar at the top of Xcode, click the device selector (it may say My Mac or another device name).
  • Select iPhone 16 Pro from the list of simulators.

Before you run the project, what do you expect to see on the simulator screen?

  • Press Cmd+R to build and run the project.

The simulator launches and displays the default ContentView: a globe icon with the text "Hello, world!" centered on screen.

What just happened?

Xcode compiled your Swift code, packaged it into an app, installed it on the simulated iPhone 16 Pro, and launched it. The default ContentView.swift contains a simple VStack with an Image and a Text view.

🙋‍♀️ Simulator not appearing?

If you see an error about a missing simulator runtime, go to Xcode > Settings > Platforms and download the iOS simulator runtime. This can take several minutes depending on your internet connection.

If the build fails with a signing error, select None for Team in the Signing & Capabilities tab (a simulator build does not require a paid Apple Developer account).

Help me troubleshoot my Xcode build or simulator issue.

Your blank canvas is ready. Next up, you'll fill it with a deliberately messy "God View" that crams data, formatting, navigation, and UI into a single massive file.

Build the God View (The Anti-Pattern)

You have a blank SwiftUI project running in the simulator. Now you need something to refactor.

In this step, you will deliberately build the worst version of this app: everything crammed into a single file. Data, formatting, navigation logic, and multiple views all tangled together. This is the exact kind of code an interviewer might hand you and ask "what would you improve?"

In this step, get ready to:
  • Replace ContentView with a massive single-file view containing hardcoded data, inline formatting, and navigation.
  • Run the app and see a list of reservations with colored status badges and a tappable detail view.
  • Identify why this "God View" pattern is untestable and fragile.

Type it, don't paste it

Throughout this project, you will see the code to write and then type it out yourself. Resist the urge to copy-paste. In your interview with Jens, you will not have code to copy. Typing forces you to read each line, understand the pattern, and build the muscle memory to reproduce it under pressure. If you can write it once here, you can write it again on Monday.

Replace ContentView with the God View data

The God View anti-pattern puts everything in one place: data definitions, UI layout, formatting logic, and navigation destinations. You will build this deliberately so you have a concrete "before" to contrast with the clean architecture later.

  • In the Xcode editor, click inside ContentView.swift to make sure it is the active file.
  • Press Cmd+A to select all the existing code, then press Delete to clear the file.
  • Type out the following code to start the God View file with the import, struct declaration, and hardcoded reservation data:
import SwiftUI

// GOD VIEW ANTI-PATTERN: Everything in one file
// Data, formatting, navigation, and multiple views all mixed together

struct ContentView: View {
    // Hardcoded data inline in the view
    let reservations: [(id: UUID, guestName: String, checkIn: Date, checkOut: Date, status: String, amount: Double, messages: [(sender: String, text: String)])] = {
        let calendar = Calendar.current
        let today = Date()
        return [
            (UUID(), "Sarah Johnson", calendar.date(byAdding: .day, value: 1, to: today)!, calendar.date(byAdding: .day, value: 4, to: today)!, "Confirmed", 892.50, [("Sarah Johnson", "Hi, is early check-in available?"), ("Partner", "Yes, we can arrange check-in from 1 PM.")]),
            (UUID(), "Michael Chen", today, calendar.date(byAdding: .day, value: 2, to: today)!, "Checked In", 1245.00, [("Michael Chen", "Could I get extra towels please?")]),
            (UUID(), "Emma Wilson", calendar.date(byAdding: .day, value: 3, to: today)!, calendar.date(byAdding: .day, value: 7, to: today)!, "Pending", 2100.00, []),
            (UUID(), "James Rodriguez", calendar.date(byAdding: .day, value: -3, to: today)!, calendar.date(byAdding: .day, value: -1, to: today)!, "Checked Out", 675.00, [("James Rodriguez", "Thank you for the wonderful stay!")]),
            (UUID(), "Lisa Park", calendar.date(byAdding: .day, value: 5, to: today)!, calendar.date(byAdding: .day, value: 8, to: today)!, "Cancelled", 1500.00, [("Lisa Park", "I need to cancel due to a schedule change.")])
        ]
    }()

What does this code do?

This declares a reservations property as an array of tuples directly inside the view struct. Each tuple holds a guest name, dates, status string, dollar amount, and messages.

In production code, data belongs in a separate model layer. Inlining it here makes the view impossible to test in isolation and means any data change risks breaking the UI.

  • Directly below the data closure, type the body of the view containing the NavigationStack, List, and the inline detail view destination:

    var body: some View {
        NavigationStack {
            List(reservations, id: \.id) { reservation in
                NavigationLink(destination:
                    // Detail view defined inline
                    ScrollView {
                        VStack(alignment: .leading, spacing: 20) {
                            VStack(alignment: .leading, spacing: 12) {
                                Text("Reservation Details").font(.title2).fontWeight(.bold)
                                HStack { Text("Status").foregroundStyle(.secondary); Spacer(); Text(reservation.status).fontWeight(.medium) }
                                HStack { Text("Check-in").foregroundStyle(.secondary); Spacer(); Text(formattedDate(reservation.checkIn)).fontWeight(.medium) }
                                HStack { Text("Check-out").foregroundStyle(.secondary); Spacer(); Text(formattedDate(reservation.checkOut)).fontWeight(.medium) }
                                HStack { Text("Amount").foregroundStyle(.secondary); Spacer(); Text(formattedAmount(reservation.amount)).fontWeight(.medium) }
                            }
                            .padding()
                            .background(Color(.systemGray6))
                            .clipShape(RoundedRectangle(cornerRadius: 12))

What is happening here?

The body starts a NavigationStack with a List of reservations. Each reservation's NavigationLink defines its detail view destination right inside the same closure.

The detail view shows status, check-in/out dates, and amount. Defining it inline means you cannot reuse or test this detail view independently.

Add the messages section and row view
  • Directly below the .clipShape(RoundedRectangle(cornerRadius: 12)) line you just typed, add the messages section and the closing of the detail view:

                            VStack(alignment: .leading, spacing: 12) {
                                Text("Messages").font(.title2).fontWeight(.bold)
                                if reservation.messages.isEmpty {
                                    Text("No messages yet").foregroundStyle(.secondary).italic()
                                } else {
                                    ForEach(reservation.messages.indices, id: \.self) { i in
                                        VStack(alignment: .leading, spacing: 4) {
                                            Text(reservation.messages[i].sender).font(.caption).fontWeight(.semibold)
                                            Text(reservation.messages[i].text).font(.body)
                                        }
                                        .padding()
                                        .background(Color(.systemGray6))
                                        .clipShape(RoundedRectangle(cornerRadius: 8))
                                    }
                                }
                            }
                        }
                        .padding()
                    }
                    .navigationTitle(reservation.guestName)
                ) {

What does the messages section do?

This renders each guest message in a styled bubble. If there are no messages, it shows placeholder text. The ) { at the bottom closes the destination parameter and opens the NavigationLink's label closure, which defines the row the learner sees in the list.

  • Type the row view and closing braces of the body directly below the ) { line:
                    VStack(alignment: .leading, spacing: 8) {
                        HStack {
                            Text(reservation.guestName).font(.headline)
                            Spacer()
                            // Status color logic inline
                            Text(reservation.status)
                                .font(.caption)
                                .fontWeight(.medium)
                                .padding(.horizontal, 8)
                                .padding(.vertical, 4)
                                .background(statusColor(reservation.status).opacity(0.15))
                                .foregroundStyle(statusColor(reservation.status))
                                .clipShape(Capsule())
                        }
                        // Date formatting inline
                        Text("\(formattedDate(reservation.checkIn)) - \(formattedDate(reservation.checkOut))")
                            .font(.subheadline)
                            .foregroundStyle(.secondary)
                        Text(formattedAmount(reservation.amount))
                            .font(.subheadline)
                            .fontWeight(.semibold)
                    }
                    .padding(.vertical, 4)
                }
            }
            .navigationTitle("Reservations")
        }
    }

What does this row code do?

This is the NavigationLink label: what each row looks like in the list. It shows the guest name, a colored status badge using statusColor(), formatted dates, and the dollar amount.

Notice the status color logic, date formatting, and currency formatting are all called from helper functions that also live in this same file. Everything is tangled together.

  • Finally, type the helper functions and closing brace of the struct below the body:

    // Formatting logic inline in the view
    func formattedDate(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: date)
    }

    func formattedAmount(_ amount: Double) -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.currencyCode = "USD"
        return formatter.string(from: NSNumber(value: amount)) ?? "$\(amount)"
    }

    func statusColor(_ status: String) -> Color {
        switch status {
        case "Confirmed": return .green
        case "Pending": return .orange
        case "Checked In": return .blue
        case "Checked Out": return .gray
        case "Cancelled": return .red
        default: return .gray
        }
    }
}

Why are these helpers a code smell?

These functions handle date formatting, currency formatting, and status-to-color mapping. Putting them directly inside the View struct means you cannot unit test them without instantiating the entire view. In later steps, you will move this logic into a ViewModel that can be tested independently.

  • Save ContentView.swift by pressing Cmd+S.
  • Press Cmd+B to build the project. You should see Build Succeeded in the Xcode toolbar.

Seeing build errors?

Check that you pasted each chunk in order with no gaps or overlaps. The most common mistake is a missing closing brace or an accidental blank line between chunks.

If you see a red error icon, click it to jump to the problem line. Compare your code against the full-file reference in the tab below. Help me fix my build error in ContentView.swift

Run the app and see the anti-pattern in action

The build succeeded, which means all your code compiles. Now run it on the simulator to see what this God View actually produces.

  • Before you run, predict: do you think you will see a styled list of reservations, or just plain text?
  • Press Cmd+R to run the app on the iPhone 16 Pro simulator.

You should see a navigation bar titled "Reservations" with a list of 5 guests. Each row shows the guest name, a colored status badge (green for Confirmed, orange for Pending, blue for Checked In, gray for Checked Out, red for Cancelled), the date range, and a dollar amount.

  • Tap any reservation in the list (try "Sarah Johnson").

You navigate to a detail screen showing the reservation info and guest messages. The navigation title shows the guest's name.

Why is this code a problem?

This single file is around 100 lines of deeply nested code mixing data, formatting, navigation, and UI. You cannot test the formatting logic without rendering the entire view.

You cannot reuse the detail view or row view anywhere else. Any change, like adding a filter or swapping the data source, risks breaking everything because there are no boundaries between concerns. This is what you will systematically fix in the next steps.

🙋‍♀️ Don't see the status badges?

Make sure the statusColor function has all five cases spelled exactly as they appear in the data: "Confirmed", "Pending", "Checked In", "Checked Out", "Cancelled".

If badges appear gray, a case mismatch is the most likely cause. Help me debug my status badge colors

✔️ Awesome, I've got everything!

Great. Double check you have saved ContentView.swift and that the app runs showing 5 reservations with colored badges.

ⓧ I'd like to double check the full code

Here is the complete ContentView.swift file. Compare your code against this reference. The file contains the data property, the body with inline detail and row views, and the three helper functions.

Scroll up and confirm you have these sections in order:

  • The reservations tuple array with 5 entries (Sarah Johnson through Lisa Park).
  • The var body: some View containing a NavigationStack with a List.
  • Inside each NavigationLink: a ScrollView destination (the detail) and a VStack label (the row).
  • Three helper functions: formattedDate, formattedAmount, and statusColor.
  • A final closing } for the struct.

You now have a working app built the wrong way on purpose. Next up, you will start pulling this apart by extracting proper model structs and a protocol-based repository.

Extract Models and Repository

You have a working app, but everything lives in one massive file. The data, the formatting, the navigation, and the views are all tangled together. You cannot test any piece in isolation, and you cannot swap one data source for another.

The first refactoring move is to separate the data layer. You will extract proper model types into their own file and create a protocol-based repository that fetches reservations. This gives you a mockable, testable data source you can point to when Jens asks how you would improve the code.

In this step, get ready to:
  • Define proper model structs with typed properties instead of raw tuples.
  • Create a protocol-based repository that decouples data fetching from the view.
  • Wire up the view to use the new repository while keeping the UI identical.
Define the data models

Right now your reservation data is an array of tuples with unnamed fields. Tuples cannot conform to protocols, have no type safety for status values, and make it impossible to iterate messages cleanly. Proper structs fix all of this.

  • Create a new Swift file by pressing Cmd+N in Xcode.
  • Select Swift File from the template list and click Next.
  • Name the file Models.swift and click Create.
  • Type the following code into Models.swift that defines the status enum and message struct:
import Foundation

// Models conform to Codable for JSON decoding from a real API
enum ReservationStatus: String, CaseIterable, Hashable, Codable {
    case confirmed = "Confirmed"
    case pending = "Pending"
    case checkedIn = "Checked In"
    case checkedOut = "Checked Out"
    case cancelled = "Cancelled"
}

struct GuestMessage: Identifiable, Hashable, Codable {
    let id: UUID
    let sender: String
    let text: String
    let timestamp: Date
}

What does this code do?

ReservationStatus is a Swift enum backed by raw String values. This replaces the loose strings like "Confirmed" that were scattered in the God View. The compiler now catches typos for you.

GuestMessage is Identifiable so SwiftUI's ForEach can iterate over messages without index hacks. It is also Hashable so it can participate in navigation and comparison.

Both types conform to Codable. This means Swift can automatically decode them from JSON when you connect a real API later. No manual parsing required.

  • Add the Reservation struct below the GuestMessage struct by typing this out:
struct Reservation: Identifiable, Hashable, Codable {
    let id: UUID
    let guestName: String
    let checkIn: Date
    let checkOut: Date
    let status: ReservationStatus
    let amount: Double
    let messages: [GuestMessage]

    static func == (lhs: Reservation, rhs: Reservation) -> Bool {
        lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

Why the custom Hashable conformance?

Reservation contains an array of GuestMessage values. Automatic Hashable synthesis would hash every message in the array, which is expensive. By manually hashing only the id, you keep lookups fast while still satisfying NavigationStack's requirement for Hashable destination values.

  • Save Models.swift.
  • Press Cmd+B to build the project.

The build will succeed. Your new types exist but nothing uses them yet.

Build failed with errors?

Check that Models.swift is listed under the PartnerDashboard target in the File Inspector (right panel). If Xcode created it outside the target, drag it into the PartnerDashboard group in the Project Navigator.

help me fix my Models.swift build error

Create the repository protocol and implementations

A protocol defines what the data layer can do without saying how. You will create two implementations: a mock that returns hardcoded data for tests and SwiftUI previews, and an API client that decodes JSON from a real endpoint. The view never knows which one it is talking to.

  • Press Cmd+N to create another new Swift file.
  • Name it ReservationRepository.swift and click Create.
  • Type the protocol definition and the start of the mock class:
import Foundation

protocol ReservationRepositoryProtocol {
    func fetchReservations() async throws -> [Reservation]
}

final class MockReservationRepository: ReservationRepositoryProtocol {
    func fetchReservations() async throws -> [Reservation] {
        try await Task.sleep(for: .milliseconds(500))

        let calendar = Calendar.current
        let today = Date()

        return [
            Reservation(
                id: UUID(),
                guestName: "Sarah Johnson",
                checkIn: calendar.date(byAdding: .day, value: 1, to: today)!,
                checkOut: calendar.date(byAdding: .day, value: 4, to: today)!,
                status: .confirmed,
                amount: 892.50,
                messages: [
                    GuestMessage(id: UUID(), sender: "Sarah Johnson", text: "Hi, is early check-in available?", timestamp: calendar.date(byAdding: .hour, value: -2, to: today)!),
                    GuestMessage(id: UUID(), sender: "Partner", text: "Yes, we can arrange check-in from 1 PM.", timestamp: calendar.date(byAdding: .hour, value: -1, to: today)!)
                ]
            ),

What does this code do?

ReservationRepositoryProtocol declares a single async throwing method. Any class that conforms to it must provide reservations, but the caller never knows whether they come from a network, a database, or hardcoded data.

MockReservationRepository conforms to that protocol and returns test data after a short delay. The delay simulates a real network call so loading states are visible later.

  • Continue adding the next two reservations directly below the Sarah Johnson entry by typing:
            Reservation(
                id: UUID(),
                guestName: "Michael Chen",
                checkIn: today,
                checkOut: calendar.date(byAdding: .day, value: 2, to: today)!,
                status: .checkedIn,
                amount: 1245.00,
                messages: [
                    GuestMessage(id: UUID(), sender: "Michael Chen", text: "Could I get extra towels please?", timestamp: calendar.date(byAdding: .minute, value: -30, to: today)!)
                ]
            ),
            Reservation(
                id: UUID(),
                guestName: "Emma Wilson",
                checkIn: calendar.date(byAdding: .day, value: 3, to: today)!,
                checkOut: calendar.date(byAdding: .day, value: 7, to: today)!,
                status: .pending,
                amount: 2100.00,
                messages: []
            ),
  • Type the final two reservations and close the function and class:
            Reservation(
                id: UUID(),
                guestName: "James Rodriguez",
                checkIn: calendar.date(byAdding: .day, value: -3, to: today)!,
                checkOut: calendar.date(byAdding: .day, value: -1, to: today)!,
                status: .checkedOut,
                amount: 675.00,
                messages: [
                    GuestMessage(id: UUID(), sender: "James Rodriguez", text: "Thank you for the wonderful stay!", timestamp: calendar.date(byAdding: .day, value: -1, to: today)!)
                ]
            ),
            Reservation(
                id: UUID(),
                guestName: "Lisa Park",
                checkIn: calendar.date(byAdding: .day, value: 5, to: today)!,
                checkOut: calendar.date(byAdding: .day, value: 8, to: today)!,
                status: .cancelled,
                amount: 1500.00,
                messages: [
                    GuestMessage(id: UUID(), sender: "Lisa Park", text: "I need to cancel due to a schedule change.", timestamp: calendar.date(byAdding: .day, value: -2, to: today)!)
                ]
            )
        ]
    }
}

Why five reservations with different statuses?

Each reservation exercises a different ReservationStatus case (confirmed, checkedIn, pending, checkedOut, cancelled). This ensures your status badge color logic is tested across every path when you run the app.

  • Save ReservationRepository.swift.
  • Press Cmd+B to build. You should see Build Succeeded in the status bar.

Seeing a type error in the repository?

Make sure ReservationRepository.swift is in the same target as Models.swift. The repository references Reservation and GuestMessage, so both files must be compiled together.

help me fix my ReservationRepository build error

  • Add the API implementation below the closing brace of MockReservationRepository in ReservationRepository.swift by typing:
final class APIReservationRepository: ReservationRepositoryProtocol {
    // Base URL for the reservations endpoint
    private let baseURL: URL

    init(baseURL: URL = URL(string: "https://api.example.com/reservations")!) {
        self.baseURL = baseURL
    }

    func fetchReservations() async throws -> [Reservation] {
        let (data, _) = try await URLSession.shared.data(from: baseURL)

        // Use ISO 8601 dates to match standard API responses
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601

        return try decoder.decode([Reservation].self, from: data)
    }
}

What does this code do?

APIReservationRepository uses URLSession to fetch JSON from a real endpoint and JSONDecoder to turn that data into [Reservation]. Because your models are Codable, the decoder handles the mapping automatically.

The .iso8601 date strategy tells the decoder to expect dates in standard API format (e.g. 2026-08-10T14:00:00Z).

💡 Why keep both implementations?

The mock gives you instant, predictable data for unit tests, SwiftUI previews, and offline development. The API implementation is the "real" path your app takes in production.

Because both conform to the same protocol, swapping between them is a one-line change. This is the power of protocol-based dependency injection.

  • Save ReservationRepository.swift.
  • Press Cmd+B to build. You should see Build Succeeded in the status bar.
Update ContentView to use the repository

The models and repository exist, but the view still uses its old hardcoded tuples. Now you will wire up the view to fetch data from the repository instead. The view still handles formatting and navigation inline. That is deliberate. You are refactoring one layer at a time.

  • In ContentView.swift, find the large let reservations property at the top of the struct (the tuple array with its closure):
    // Hardcoded data inline in the view
    let reservations: [(id: UUID, guestName: String, checkIn: Date, checkOut: Date, status: String, amount: Double, messages: [(sender: String, text: String)])] = {
        let calendar = Calendar.current
        let today = Date()
  • Delete the entire let reservations property (from the comment above it through the closing }()). Type this single line in its place:
    @State private var reservations: [Reservation] = []
  • Find the line List(reservations, id: \.id) and replace it with:
            List(reservations) { reservation in

Why remove the id parameter?

Your Reservation struct conforms to Identifiable, so SwiftUI can find the id property automatically. The explicit id: \.id key path is no longer needed.

  • Find every occurrence of Text(reservation.status) (there are two: one in the detail view and one in the row). Replace each with:
Text(reservation.status.rawValue)
  • Find the index-based messages loop:
                                    ForEach(reservation.messages.indices, id: \.self) { i in
                                        VStack(alignment: .leading, spacing: 4) {
                                            Text(reservation.messages[i].sender).font(.caption).fontWeight(.semibold)
                                            Text(reservation.messages[i].text).font(.body)
  • Replace those four lines with the cleaner Identifiable-based iteration:
                                    ForEach(reservation.messages) { message in
                                        VStack(alignment: .leading, spacing: 4) {
                                            Text(message.sender).font(.caption).fontWeight(.semibold)
                                            Text(message.text).font(.body)
  • Find the statusColor function signature:
    func statusColor(_ status: String) -> Color {
        switch status {
        case "Confirmed": return .green
        case "Pending": return .orange
        case "Checked In": return .blue
        case "Checked Out": return .gray
        case "Cancelled": return .red
        default: return .gray
        }
    }
  • Replace the entire function with the enum-based version:
    func statusColor(_ status: ReservationStatus) -> Color {
        switch status {
        case .confirmed: return .green
        case .pending: return .orange
        case .checkedIn: return .blue
        case .checkedOut: return .gray
        case .cancelled: return .red
        }
    }

Why did the default case disappear?

Swift enums are exhaustive. When you switch on a ReservationStatus and cover every case, the compiler knows no other value is possible. No default needed. If you add a new status later, the compiler will force you to handle it everywhere.

  • Finally, find .navigationTitle("Reservations") at the bottom of the NavigationStack and add a .task modifier directly below it:
            .navigationTitle("Reservations")
            .task {
                // Swap MockReservationRepository for APIReservationRepository to use live data
                reservations = (try? await MockReservationRepository().fetchReservations()) ?? []
            }
  • Also update the comment at the top of the struct by typing:
// Still a God View but now uses MockReservationRepository for data

What does .task do?

The .task modifier runs an async closure when the view first appears. It automatically cancels if the view disappears. This is SwiftUI's recommended way to kick off data loading without blocking the main thread.

  • Save ContentView.swift.

Before you run the app, what do you expect to see? The data moved to a new file and is fetched asynchronously now. Will the UI look different, or the same?

  • Press Cmd+R to build and run the app on the iPhone 16 Pro simulator.

The UI looks identical. Five reservations, colored status badges, tappable detail views with messages. Nothing changed visually, but the data now comes from a protocol-based repository in a separate file.

Seeing an empty list or a crash?

If the list is empty, make sure you added the .task modifier to the NavigationStack content. Without it, reservations stays as an empty array and nothing loads.

If you see a compiler error about statusColor, check that you updated its parameter type from String to ReservationStatus and removed the default case.

help me fix my ContentView after connecting the repository

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files.

ⓧ I'd like to double check the full code

Since each file is large, scroll up and confirm the following chunks are present in order:

  • Models.swift: ReservationStatus enum, GuestMessage struct, Reservation struct with custom Hashable conformance.
  • ReservationRepository.swift: ReservationRepositoryProtocol protocol, MockReservationRepository class with 5 reservations (Sarah, Michael, Emma, James, Lisa).
  • ContentView.swift: @State private var reservations: [Reservation] = [] property, List(reservations) without id: parameter, reservation.status.rawValue for display, ForEach(reservation.messages) { message in for iteration, enum-based statusColor function, and .task modifier calling the repository.

You have successfully separated the data layer from the view. The app still looks the same, but under the hood you can now point to a clean protocol that any repository can implement. In the next step, you will pull the formatting logic and loading state into an @Observable ViewModel, making the view truly thin and the logic fully testable.

Refactor to MVVM with @Observable

In the last step, you pulled the data out of your God View into a separate repository. The view itself still owns all the business logic: formatting dates, formatting currency, managing loading state, and deciding when to fetch.

An interviewer like Jens can spot this immediately. The fix is an @Observable ViewModel that owns the logic while the view becomes a thin layout shell. This is the core MVVM refactoring pattern used in modern SwiftUI codebases.

In this step, get ready to:
  • Create an @Observable ViewModel that owns data-fetching and formatting logic.
  • Replace the God View with a thin ReservationListView that only does layout.
  • Extract ReservationRowView and ReservationDetailView into focused, reusable components.
Create the ViewModel

The ViewModel is the single class responsible for fetching reservations and exposing formatted data to the view. By marking it @Observable, SwiftUI automatically re-renders only the views that read properties that changed.

  • Create a new Swift file by pressing Cmd+N in Xcode.
  • Select Swift File and name it ReservationListViewModel.swift.
  • Type the class definition with properties and the loading method:
import Foundation

@Observable
class ReservationListViewModel {
    // The list of reservations displayed in the view
    var reservations: [Reservation] = []
    var isLoading = false

    // Repository injected via init for testability
    private let repository: ReservationRepositoryProtocol

    init(repository: ReservationRepositoryProtocol) {
        self.repository = repository
    }

    // Fetches reservations and handles errors gracefully
    func loadReservations() async {
        isLoading = true
        do {
            reservations = try await repository.fetchReservations()
        } catch {
            reservations = []
        }
        isLoading = false
    }
}

What does this code do?

  • @Observable tells SwiftUI to track property changes automatically. No @Published wrappers needed.
  • The repository is injected through the initializer. This means you can swap in a mock for testing or a real network layer for production.
  • loadReservations() is async because the repository call uses await. It catches errors and falls back to an empty array instead of crashing.
  • Now type the formatting methods below the closing brace of loadReservations() (still inside the class):
    // Format check-in and check-out dates as a readable range
    func formattedDateRange(for reservation: Reservation) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return "\(formatter.string(from: reservation.checkIn)) - \(formatter.string(from: reservation.checkOut))"
    }

    // Format the reservation amount as USD currency
    func formattedAmount(for reservation: Reservation) -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.currencyCode = "USD"
        return formatter.string(from: NSNumber(value: reservation.amount)) ?? "$\(reservation.amount)"
    }

What do these methods do?

These formatting methods previously lived inside ContentView.swift as view methods. By moving them into the ViewModel, the view no longer needs to know how dates or currencies are formatted. It just asks the ViewModel for a string.

  • Save ReservationListViewModel.swift.
  • Build the project by pressing Cmd+B.

You should see Build Succeeded in the Xcode toolbar. The ViewModel compiles independently because it only depends on Models.swift and ReservationRepository.swift, which already exist.

Build failed?

  • Check that the file is added to the PartnerDashboard target. Select the file in the navigator, open the File Inspector (right panel), and confirm the target membership checkbox is checked.
  • Make sure import Foundation is at the top. The @Observable macro comes from the Observation framework which Foundation re-exports.

Why isn't my ViewModel compiling in Xcode?

Create the list view and row view

Now you will replace the God View with a thin ReservationListView that only handles layout. Each row gets its own reusable component too.

  • Create a new Swift file (Cmd+N > Swift File) and name it ReservationRowView.swift.
  • Type the row view that displays one reservation's summary:
import SwiftUI

struct ReservationRowView: View {
    let reservation: Reservation
    let dateRange: String
    let amount: String

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Text(reservation.guestName)
                    .font(.headline)
                Spacer()
                // Colored status badge component
                StatusBadgeView(status: reservation.status)
            }

            Text(dateRange)
                .font(.subheadline)
                .foregroundStyle(.secondary)

            HStack {
                Text(amount)
                    .font(.subheadline)
                    .fontWeight(.semibold)
                Spacer()
                // Show message count if any exist
                if !reservation.messages.isEmpty {
                    Label("\(reservation.messages.count)", systemImage: "message.fill")
                        .font(.caption)
                        .foregroundStyle(.blue)
                }
            }
        }
        .padding(.vertical, 4)
    }
}

What does this view do?

ReservationRowView receives pre-formatted strings from the ViewModel (dateRange and amount). It does not format anything itself. This keeps the view purely about layout and makes it trivially reusable.

  • Below ReservationRowView, type the StatusBadgeView in the same file:
struct StatusBadgeView: View {
    let status: ReservationStatus

    var body: some View {
        Text(status.rawValue)
            .font(.caption)
            .fontWeight(.medium)
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
            .background(backgroundColor.opacity(0.15))
            .foregroundStyle(backgroundColor)
            .clipShape(Capsule())
    }

    // Maps each status to its display color
    private var backgroundColor: Color {
        switch status {
        case .confirmed: return .green
        case .pending: return .orange
        case .checkedIn: return .blue
        case .checkedOut: return .gray
        case .cancelled: return .red
        }
    }
}

Why extract StatusBadgeView?

In the God View, the status color logic was buried inside the row's layout code. Now it is a self-contained component. If Jens asks you to add a new status, you edit one file and one switch case.

  • Save ReservationRowView.swift.
  • Create another new Swift file (Cmd+N > Swift File) and name it ReservationListView.swift.
  • Type the thin list view that delegates all logic to the ViewModel:
import SwiftUI

struct ReservationListView: View {
    // ViewModel is view-owned via @State
    @State private var viewModel = ReservationListViewModel(repository: MockReservationRepository())

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading && viewModel.reservations.isEmpty {
                    ProgressView("Loading reservations...")
                } else {
                    List(viewModel.reservations) { reservation in
                        NavigationLink(value: reservation) {
                            ReservationRowView(
                                reservation: reservation,
                                dateRange: viewModel.formattedDateRange(for: reservation),
                                amount: viewModel.formattedAmount(for: reservation)
                            )
                        }
                    }
                }
            }
            .navigationTitle("Reservations")
            .navigationDestination(for: Reservation.self) { reservation in
                ReservationDetailView(reservation: reservation)
            }
            .task {
                await viewModel.loadReservations()
            }
        }
    }
}

What changed compared to the God View?

  • @State private var viewModel tells SwiftUI to own and persist the ViewModel instance across re-renders.
  • NavigationLink(value:) with .navigationDestination(for:) replaces the inline destination closure. This is the type-safe navigation pattern introduced in iOS 16.
  • The view body contains zero formatting logic. It asks the ViewModel for strings and passes them to child views.
  • Save ReservationListView.swift.
  • Build with Cmd+B. You will see an error because ReservationDetailView does not exist yet.

That error is expected. You will create the detail view next.

Extract the detail view and wire up the app

The last piece is the detail screen. In the God View this was an inline closure inside NavigationLink. Now it becomes its own file with small helper components.

  • Create a new Swift file (Cmd+N > Swift File) and name it ReservationDetailView.swift.
  • Type the main detail view struct:
import SwiftUI

struct ReservationDetailView: View {
    let reservation: Reservation

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 20) {
                reservationInfoSection
                messagesSection
            }
            .padding()
        }
        .navigationTitle(reservation.guestName)
        .navigationBarTitleDisplayMode(.large)
    }

    // Card showing status, dates, and amount
    private var reservationInfoSection: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Reservation Details")
                .font(.title2)
                .fontWeight(.bold)

            InfoRow(label: "Status", value: reservation.status.rawValue)
            InfoRow(label: "Check-in", value: formatted(date: reservation.checkIn))
            InfoRow(label: "Check-out", value: formatted(date: reservation.checkOut))
            InfoRow(label: "Amount", value: formattedAmount)
        }
        .padding()
        .background(Color(.systemGray6))
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

What does this code do?

The body is just two computed properties stacked vertically. Each section is a private computed property, keeping the body readable at a glance. InfoRow is a helper struct you will define next.

  • Still inside the ReservationDetailView struct, type the messages section and formatting helpers below reservationInfoSection:
    // Displays the guest message thread
    private var messagesSection: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Messages")
                .font(.title2)
                .fontWeight(.bold)

            if reservation.messages.isEmpty {
                Text("No messages yet")
                    .foregroundStyle(.secondary)
                    .italic()
            } else {
                ForEach(reservation.messages) { message in
                    MessageBubbleView(message: message)
                }
            }
        }
    }

    private func formatted(date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        return formatter.string(from: date)
    }

    private var formattedAmount: String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.currencyCode = "USD"
        return formatter.string(from: NSNumber(value: reservation.amount)) ?? "$\(reservation.amount)"
    }

What does messagesSection do?

It conditionally shows either a placeholder or a list of MessageBubbleView components. Each message gets its own visual bubble. The formatting helpers are private because only this view needs them.

  • Below the closing brace of ReservationDetailView, type the two helper structs in the same file:
struct InfoRow: View {
    let label: String
    let value: String

    var body: some View {
        HStack {
            Text(label)
                .foregroundStyle(.secondary)
            Spacer()
            Text(value)
                .fontWeight(.medium)
        }
    }
}

struct MessageBubbleView: View {
    let message: GuestMessage

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack {
                Text(message.sender)
                    .font(.caption)
                    .fontWeight(.semibold)
                Spacer()
                Text(message.timestamp, style: .relative)
                    .font(.caption2)
                    .foregroundStyle(.secondary)
            }
            Text(message.text)
                .font(.body)
        }
        .padding()
        .background(Color(.systemGray6))
        .clipShape(RoundedRectangle(cornerRadius: 8))
    }
}

Why separate InfoRow and MessageBubbleView?

Each struct has a single job. InfoRow draws a label-value pair. MessageBubbleView draws one chat message. If you need either pattern elsewhere in the app, you can reuse them without copy-pasting.

  • Save ReservationDetailView.swift.

Now you need to point the app entry point at your new ReservationListView instead of the old ContentView.

  • Open PartnerDashboardApp.swift and find this line:
            ContentView()
  • Replace ContentView() with ReservationListView() so the line looks like this:
            ReservationListView()
  • Save PartnerDashboardApp.swift.
  • Delete ContentView.swift from the project. Right-click the file in the navigator and select Delete, then choose Move to Trash.

Before you run, what do you expect to see? The UI should look exactly the same as before. The architecture underneath is completely different.

  • Run the app on the iPhone 16 Pro simulator by pressing Cmd+R.

You should see the same list of 5 reservations with colored status badges. Tap any reservation to see the detail view with guest info and messages.

What just happened?

The app looks identical, but the code behind it is radically different. Your single 200-line God View is now spread across focused files: a ViewModel handling logic, a list view handling layout, a row component, and a detail component.

This is the story you tell Jens: same behavior, better architecture, each piece testable in isolation.

🙋‍♀️ App crashes or shows a blank screen?

  • Check that PartnerDashboardApp.swift references ReservationListView() and not ContentView(). If ContentView.swift is already deleted, referencing it causes a compile error.
  • Make sure Reservation conforms to Hashable in Models.swift. The .navigationDestination(for: Reservation.self) modifier requires it.
  • Verify that all new files have the PartnerDashboard target checked in the File Inspector.

Help me debug why my refactored SwiftUI app is crashing or showing a blank screen after extracting the ViewModel and views.

✔️ Awesome, I've got everything!

Great. Double check you have saved all five files: ReservationListViewModel.swift, ReservationListView.swift, ReservationRowView.swift, ReservationDetailView.swift, and PartnerDashboardApp.swift.

ⓧ I'd like to double check the full code

Scroll up and confirm each of these chunks is present in the correct file, in order:

  • ReservationListViewModel.swift: Class definition with reservations, isLoading, init, loadReservations(), formattedDateRange(for:), and formattedAmount(for:).
  • ReservationRowView.swift: ReservationRowView struct followed by StatusBadgeView struct.
  • ReservationListView.swift: Single struct with @State private var viewModel, NavigationStack, .navigationDestination, and .task modifier.
  • ReservationDetailView.swift: ReservationDetailView with reservationInfoSection, messagesSection, formatting helpers, then InfoRow and MessageBubbleView structs.
  • PartnerDashboardApp.swift: Entry point using ReservationListView().

Why @Observable instead of ObservableObject?

@Observable (iOS 17+) replaces the older ObservableObject protocol. With ObservableObject, every @Published property triggered a re-render of the entire view body. With @Observable, SwiftUI tracks exactly which properties each view reads and only re-renders views that depend on properties that actually changed.

This makes your app faster by default. It also removes boilerplate. You can tell Jens: fewer lines, better performance, same pattern.

Your architecture is now clean MVVM. The view is thin, the ViewModel is testable, and every component has a single responsibility. Next up, you will prove this architecture works by writing unit tests that exercise the ViewModel without touching the UI.

Add Unit Tests for the ViewModel

Your app now has clean MVVM architecture with a thin view, an @Observable ViewModel, and a protocol-based repository. The architecture looks professional, but you need proof it actually works independently of the UI.

In this step, you will add unit tests that exercise the ViewModel in complete isolation. When Jens asks about testing and maintainability, you can point to these tests and say: "Because I injected the repository as a protocol, I can test the ViewModel with a mock and zero UI dependencies."

In this step, get ready to:
  • Add a Unit Testing Bundle target to the project.
  • Write a mock test repository that returns controlled data.
  • Write and run three passing unit tests proving the ViewModel works independently.
Add a test target to your project

Xcode keeps test code in a separate target so it never ships with your app binary. You need to create this target before writing any tests.

  • In Xcode, click File in the menu bar.
  • Select New, then Target.
  • In the template chooser, make sure iOS is selected at the top.
  • Scroll down to the Test section and select Unit Testing Bundle.
  • Click Next.
  • Set the Product Name to PartnerDashboardTests.
  • Make sure Testing System is set to Swift Testing.
  • Click Finish.

You should now see a PartnerDashboardTests folder in the Project Navigator with a test file inside it.

Why a separate target?

Test targets can import your app's code with @testable import, which exposes internal symbols for testing. The test code never gets bundled into the app you ship to users.

🙋‍♀️ Don't see the new folder?

Make sure you clicked Finish and not Cancel in the target dialog. If the folder is missing, try the process again from File > New > Target.

help me add a Unit Testing Bundle target to my Xcode project

Create the mock repository and first test

A mock repository lets you control exactly what data the ViewModel receives. This removes network delays and randomness, making tests fast and deterministic.

  • Open the test file inside the PartnerDashboardTests folder (it may be named PartnerDashboardTests.swift).
  • Select all the template code and type the following in its place:
import Testing
@testable import PartnerDashboard

// A lightweight mock that returns whatever reservations you give it
final class MockTestRepository: ReservationRepositoryProtocol {
    var reservationsToReturn: [Reservation] = []

    func fetchReservations() async throws -> [Reservation] {
        return reservationsToReturn
    }
}

struct PartnerDashboardTests {
    // Verify that loading populates the reservations array
    @Test func testLoadReservations_populatesArray() async throws {
        let mockRepo = MockTestRepository()
        mockRepo.reservationsToReturn = [
            Reservation(id: UUID(), guestName: "Test Guest", checkIn: Date(), checkOut: Date(), status: .confirmed, amount: 100.0, messages: [])
        ]
        let viewModel = ReservationListViewModel(repository: mockRepo)
        await viewModel.loadReservations()
        #expect(viewModel.reservations.count == 1)
        #expect(viewModel.reservations.first?.guestName == "Test Guest")
    }
}

What does this code do?

  • MockTestRepository conforms to ReservationRepositoryProtocol but returns a controllable array instead of hardcoded hotel data.
  • @testable import PartnerDashboard gives the test file access to all internal types from your app target.
  • The first test creates a ViewModel with one test reservation, calls loadReservations(), and asserts the data arrived correctly using #expect.
  • Save the file with Cmd+S.
  • Run the tests by pressing Cmd+U.

You should see a green checkmark next to testLoadReservations_populatesArray in the Test Navigator, confirming the ViewModel loads data correctly.

Test failed or won't compile?

If you see "Cannot find type 'ReservationRepositoryProtocol'", the test target cannot see your app code. Click the PartnerDashboardTests target in your project settings, go to General, and confirm PartnerDashboard is listed under Target Dependencies.

help me fix my Swift Testing target that cannot find types from my main app target

Add the remaining tests and verify all pass

One passing test is good. Three tests covering different scenarios proves the architecture handles edge cases.

  • In PartnerDashboardTests.swift, find the closing brace } of the testLoadReservations_populatesArray function.
  • Type the following two tests below it, still inside the PartnerDashboardTests struct:
    // Verify that an empty repository results in an empty array
    @Test func testLoadReservations_emptyRepository_returnsEmpty() async throws {
        let mockRepo = MockTestRepository()
        mockRepo.reservationsToReturn = []
        let viewModel = ReservationListViewModel(repository: mockRepo)
        await viewModel.loadReservations()
        #expect(viewModel.reservations.isEmpty)
    }

    // Verify that the count matches exactly what the repository provides
    @Test func testReservationCount_matchesRepository() async throws {
        let mockRepo = MockTestRepository()
        mockRepo.reservationsToReturn = [
            Reservation(id: UUID(), guestName: "Guest A", checkIn: Date(), checkOut: Date(), status: .confirmed, amount: 200.0, messages: []),
            Reservation(id: UUID(), guestName: "Guest B", checkIn: Date(), checkOut: Date(), status: .pending, amount: 300.0, messages: []),
            Reservation(id: UUID(), guestName: "Guest C", checkIn: Date(), checkOut: Date(), status: .checkedIn, amount: 400.0, messages: [])
        ]
        let viewModel = ReservationListViewModel(repository: mockRepo)
        await viewModel.loadReservations()
        #expect(viewModel.reservations.count == 3)
    }

What do these tests prove?

testLoadReservations_emptyRepository_returnsEmpty confirms the ViewModel handles the empty case gracefully without crashing.

testReservationCount_matchesRepository verifies that when the repository returns 3 items, the ViewModel exposes exactly 3. Together, these tests cover the happy path, the empty edge case, and the data-integrity contract.

  • Save the file with Cmd+S.

Before you run the tests, predict: do you think all three will pass, or will one fail? Why?

  • Run all tests by pressing Cmd+U.

All three tests pass green in the Test Navigator. This proves the ViewModel is fully decoupled from the UI, the protocol-based dependency injection enables isolated testing, and the architecture is maintainable.

Seeing a test failure?

Check that the two new tests are inside the PartnerDashboardTests struct braces, not outside them. A misplaced closing brace } is the most common cause of unexpected compile errors here.

help me debug why my Swift Testing tests are failing or not compiling

✔️ Awesome, I've got everything!

Great. Confirm you have saved PartnerDashboardTests.swift and all three tests pass green.

ⓧ I'd like to double check the full code

Your PartnerDashboardTests.swift file should contain these sections in order:

  • import Testing and @testable import PartnerDashboard at the top.
  • The MockTestRepository class with a reservationsToReturn property and fetchReservations() method.
  • The PartnerDashboardTests struct containing three @Test functions: testLoadReservations_populatesArray, testLoadReservations_emptyRepository_returnsEmpty, and testReservationCount_matchesRepository.

Scroll up and confirm each chunk matches. If anything is off, copy the relevant chunk again.

Why this matters for Monday's interview

You can now tell Jens: "I designed the repository as a protocol so I could inject a mock in tests. The ViewModel has zero UI dependencies, which means I can verify business logic without spinning up a simulator."

This is the exact "testing and maintainability" signal Expedia's rubric looks for.

Your architecture is now proven testable with three green checks. Next up in the Secret Mission, you will extend this architecture to handle real-world async complexity with pull-to-refresh, loading states, and error handling.

Secret mission

Add Pull-to-Refresh with Loading and Error States

Your dashboard works, but what happens when the network drops? Real production apps handle loading, success, and error states gracefully. Extend your architecture with a ViewState enum, pull-to-refresh, and a retry button that Jens will love to see in your interview.

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:

  • The PartnerDashboard Xcode project folder on your Mac (includes all Swift source files, test target, and build artifacts).

Keep everything running

No action needed. Choose this if you want to keep the project as a reference for Monday's interview with Jens.

  • Your PartnerDashboard project stays on your Mac ready to open in Xcode anytime.
  • You can re-run the app or tests whenever you want to practice walking through the architecture.

Pause - I'll come back to this later

Since this is a local Xcode project with no running processes or cloud services, there is nothing to pause. The project sits idle on your filesystem until you open it again.

  • Close the project in Xcode by pressing Cmd+W.
  • The project folder remains on disk with no background activity.

Delete - I don't want to use this again

Remove the entire project from your Mac to start fresh.

  • Close the project in Xcode if it is still open.
  • Open Terminal and delete the project folder by running:
rm -rf ~/Desktop/PartnerDashboard

Saved it somewhere else?

If you saved the project to a different location when creating it in Xcode, replace ~/Desktop/PartnerDashboard with the actual path to your project folder.

  • Confirm it is gone by running:
ls ~/Desktop | grep PartnerDashboard

You should see no output, confirming the folder has been removed.

Nice Work!

Nice Work!

Nice work! You just built a SwiftUI Partner Dashboard from scratch, deliberately broke it with a God View anti-pattern, and systematically refactored it into clean, testable architecture.

You've learned how to:

  • Identify and articulate SwiftUI code smells by building a deliberate God View anti-pattern, then refactoring it into clean MVVM architecture with an @Observable ViewModel and protocol-based dependency injection.
  • Extract reusable view components (ReservationRowView, ReservationDetailView) and implement NavigationStack with type-safe navigation for the list-to-detail flow.
  • Write unit tests proving the ViewModel works independently via an injected mock repository, demonstrating testability and maintainability to an interviewer.
  • Secret Mission: Added pull-to-refresh with a ViewState enum handling loading, success, and error states with full test coverage.

Ready to quiz yourself?