Skip to content

MVVM and presentation models

A presentation model earns its place when a screen has derived display state, user commands, and asynchronous work that should behave the same in SwiftUI and UIKit. It is not a mandatory partner for every view.

Model the editor contract

@MainActor
final class NoteEditorModel {
private let saveNote: (NoteDraft) async throws -> Void
var draft = NoteDraft(title: "", body: "")
private(set) var phase: Phase = .editing
enum Phase: Equatable {
case editing
case saving
case failed(String)
case saved
}
init(saveNote: @escaping (NoteDraft) async throws -> Void) {
self.saveNote = saveNote
}
var canSave: Bool {
!draft.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& phase != .saving
}
func save() async {
guard canSave else { return }
phase = .saving
do {
try await saveNote(draft)
phase = .saved
} catch {
phase = .failed("The note could not be saved.")
}
}
}

SwiftUI can observe the model and UIKit can render it after commands. Neither adapter changes the saving policy.

Keep UI mechanics outside

The model should not import SwiftUI or UIKit to express colors, alerts, navigation controllers, focus bindings, or table cells. It exposes semantic state such as saving or failed; each UI translates that state into its own presentation.

The model is also not the domain. A title invariant still belongs in a domain value or use case if it must hold for widgets, imports, and background sync.

Add the boundary selectively

A static row probably needs no model. An editor with validation, autosave, cancellation, and two UI adapters does. Judge the boundary by reduced duplication and easier behavior tests, not by whether the folder tree says MVVM.

Series navigation

References