Skip to content
Prepare to Grow

iOS Architecture

MVVM in UIKit and SwiftUI

Design presentation models with explicit state and actions without turning the view model into a second application layer.

Advanced6 min read2 guided activities

Before you begin

What you will understand

  • Define View, ViewModel, and Model responsibilities.
  • Represent complete screen state explicitly.
  • Connect a view model to UIKit and SwiftUI appropriately.
  • Avoid oversized, framework-coupled view models.
01

Learning stage

Concept Explanation

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.

  • View: renders state and forwards user intent.
  • View model: owns presentation state and coordinates screen actions.
  • Model: domain values, rules, and external data capabilities.
  • State machine: explicit valid states and transitions.
  • One-way flow: action enters, state changes, view renders.
02

Learning stage

Syntax & Examples

One framework-neutral presentation model can drive either UIKit rendering or SwiftUI observation.

CourseListViewModel.swiftswift
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.")
    }
  }
}
The view model expresses user-facing states and actions while loading remains behind a dependency.
03

Learning stage

Real-World Usage

Connect the architectural concept to maintainable iOS features and real change scenarios.

  • Drive consistent loading, empty, content, and error presentation.
  • Unit test retries, cancellation, and state transitions.
  • Share presentation logic during UIKit-to-SwiftUI migration when appropriate.
  • Keep reusable domain decisions outside framework-specific views.
04

Learning stage

Common Mistakes

Identify abstractions and dependencies that add coupling, ambiguity, or unnecessary ceremony.

  • Exposing a dozen independent booleans that form contradictory states.
  • Importing UIKit into a view model only to change labels directly.
  • Putting every app service and route in one global view model.
  • Starting duplicate tasks every time a view appears.
  • Assuming @Observable automatically determines correct state ownership.
05

Learning stage

Practice Exercises

Apply the design to a small feature and evaluate it through observable change scenarios.

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.

Activity 2

Connect two renderers

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.

06

Learning stage

Knowledge Check

Answer each question before revealing the explanation.

QUESTION 1

What should a view model primarily own?

  1. A.Presentation state and coordination of screen actions
  2. B.Every application dependency
  3. C.UIKit view hierarchy
  4. D.Database migrations
Reveal answer

Presentation state and coordination of screen actions

Domain decisions and infrastructure remain separate when they have independent responsibilities.

QUESTION 2

Why model loading, content, empty, and failure as an enum?

Reveal answer

It makes mutually exclusive states explicit and prevents contradictory boolean combinations.

The view can render one complete state exhaustively.

07

Learning stage

Interview Preparation

Explain the decision, dependency direction, trade-offs, and evidence—not only the pattern name.

How does MVVM differ between UIKit and SwiftUI?

Short answer: The responsibilities can be similar, but UIKit needs explicit observation/rendering while SwiftUI derives updates through its data-flow system.

Explore the detailed answer

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

  • • When is MVVM unnecessary?
  • • Who should create the view model?
08

Learning stage

Key Takeaways

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.

  • Views render state and emit actions.
  • View models coordinate presentation behavior.
  • Use one complete state instead of conflicting flags.
  • UIKit and SwiftUI differ mainly in binding and rendering mechanics.
  • Keep domain, infrastructure, and navigation responsibilities deliberate.