Let's think about it this way for a second
MVVM (Model-View-ViewModel) is an architecture pattern that separates the UI (View) from business logic (ViewModel) — Model is the data (database, API), View is the UI (SwiftUI View), and ViewModel is the middleman between the two. Adding a `@Published` property on a class that conforms to `ObservableObject` means that every time the value changes, all Views observing this ViewModel via `@ObservedObject`/`@StateObject` get automatically re-rendered — a wider scope than `@State`'s View-local scope.
Let's connect this to a real-world scenario
If you write `class TodoViewModel: ObservableObject { @Published var todos: [TodoItem] = []; func addTodo(title: String) { todos.append(TodoItem(title: title)) } }` — in your View, you can grab the instance with `@StateObject private var viewModel = TodoViewModel()` and display `viewModel.todos` in a `List`. On a button tap, you just call `viewModel.addTodo(...)` — no more mixing business logic into your UI code.
Let's look at it together
class TodoViewModel: ObservableObject {
@Published var todos: [TodoItem] = []
func addTodo(title: String) {
todos.append(TodoItem(title: title))
}
}
struct TodoScreen: View {
@StateObject private var viewModel = TodoViewModel()
var body: some View {
List(viewModel.todos) { todo in
Text(todo.title)
}
}
}Calling `viewModel.addTodo(title: "Buy milk")` shows a new item automatically appearing in the List.5-minute try-it
Write `TodoViewModel` yourself and add 2-3 todo items using `addTodo()` — run `TodoScreen` and confirm they automatically show up in the List.
A quick heads-up
Updating a `@Published` property from a background thread (not the UI thread) can cause a crash or undefined behavior — you need to annotate your ViewModel class with `@MainActor` (or use `DispatchQueue.main.async`).