Question 1
MVVM — where logic lives in UIKit vs SwiftUI
Your team is standardizing on MVVM. How do you decide what belongs in the view controller or view vs the view model? What mistakes create ‘fat’ view models or leaked UIKit in the domain layer?
Follow-ups
- How do you handle navigation from the view model without tight coupling?
Answer outline
Treat the view model as presentation state and user intent: formatting for display, enabling buttons, mapping domain errors to user-visible messages, and exposing async commands the view triggers.
It should be agnostic of UI frameworks, e.g. UILabel, layout constraints, or UIImage construction details.
The view owns lifecycle, animations, and wiring, it forwards events to the VM and renders @Published / @Observable / bindings.
Massive view controllers usually mean domain logic and networking stayed in the view controller; move those behind protocols the view model calls.
Principles
- Domain models stay framework-agnostic; mapping happens at the VM or a mapper type.
- SwiftUI: keep view models on the
@MainActor. ObservableObject+@Published(Combine) drives updates throughobjectWillChange;@Observable(Observation) is a class macro that synthesizes change tracking for stored properties.
protocol ProfileViewModeling: AnyObject {
var displayName: String { get }
func load()
func saveTapped()
}
final class ProfileViewController: UIViewController {
private let viewModel: ProfileViewModeling
init(viewModel: ProfileViewModeling) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
// bind labels in viewDidLoad; forward actions only
}
@Observable
final class CounterModel {
private(set) var count = 0
func increment() { count += 1 }
}
struct CounterView: View {
@Bindable var model: CounterModel
var body: some View {
Text("\(model.count)")
Button(" + ") { model.increment() }
}
}
Follow-up angles
- Navigation: use coordinators, closures, or router protocols so the VM emits routes (
enum Route) instead of pushing VCs directly.