Activity 1
Replace boolean UI state
Convert isLoading, hasError, isEmpty, and items into one state enum and update the renderer for every case.
Hint: List invalid boolean combinations first to see what the enum removes.
iOS Architecture
Design presentation models with explicit state and actions without turning the view model into a second application layer.
Before you begin
Learning stage
MVVM introduces a presentation model between a rendered view and domain or data collaborators. The view sends user actions and renders state; the view model coordinates presentation behavior and transforms results into values the screen can display. Domain rules and infrastructure do not automatically belong in the view model simply because it is not a view.
Model screen state so impossible combinations are difficult to express. An enum such as idle, loading, content, empty, and failed is often clearer than separate booleans for spinner visibility, error visibility, and content availability. User actions become methods or action values whose effects update this single state.
UIKit needs an observation mechanism or render callback to receive changes. SwiftUI can observe an @Observable reference stored by the correct owner and update views when accessed properties change. In both frameworks, instantiate the view model at a stable feature boundary, inject dependencies, isolate UI-facing mutation to the main actor, and test state transitions without rendering UI.
Learning stage
One framework-neutral presentation model can drive either UIKit rendering or SwiftUI observation.
import Observation
@MainActor @Observable
final class CourseListViewModel {
enum State: Equatable {
case idle
case loading
case content([CourseRow])
case empty
case failed(message: String)
}
private let loader: any CourseLoading
private(set) var state: State = .idle
init(loader: any CourseLoading) {
self.loader = loader
}
func appeared() async {
guard state == .idle else { return }
await reload()
}
func retryTapped() async { await reload() }
private func reload() async {
state = .loading
do {
let courses = try await loader.courses()
let rows = courses.map(CourseRow.init)
state = rows.isEmpty ? .empty : .content(rows)
} catch is CancellationError {
state = .idle
} catch {
state = .failed(message: "Try again in a moment.")
}
}
}Learning stage
Connect the architectural concept to maintainable iOS features and real change scenarios.
Learning stage
Identify abstractions and dependencies that add coupling, ambiguity, or unnecessary ceremony.
Learning stage
Apply the design to a small feature and evaluate it through observable change scenarios.
Activity 1
Convert isLoading, hasError, isEmpty, and items into one state enum and update the renderer for every case.
Hint: List invalid boolean combinations first to see what the enum removes.
Activity 2
Render the same presentation states in a UIKit controller and a small SwiftUI view while keeping navigation outside the model.
Hint: The binding mechanism may differ even if state meaning stays the same.
Learning stage
Answer each question before revealing the explanation.
QUESTION 1
Presentation state and coordination of screen actions
Domain decisions and infrastructure remain separate when they have independent responsibilities.
QUESTION 2
It makes mutually exclusive states explicit and prevents contradictory boolean combinations.
The view can render one complete state exhaustively.
Learning stage
Explain the decision, dependency direction, trade-offs, and evidence—not only the pattern name.
Short answer: The responsibilities can be similar, but UIKit needs explicit observation/rendering while SwiftUI derives updates through its data-flow system.
In both cases the model receives actions and exposes screen state. UIKit commonly subscribes or receives a render callback and mutates controls. SwiftUI reads observable state declaratively. I keep ownership stable, make transitions testable, isolate main-actor mutation, and avoid putting domain or navigation responsibilities in the view model by default.
Possible follow-ups
Learning stage
MVVM is useful when a screen has meaningful presentation state and transitions. Its value comes from explicit ownership and testable flow, not from adding a ViewModel suffix.