Why bother?
Most iOS apps start life as a tangle of view controllers. It works — until it doesn’t. The moment you need to write a test, or hand the codebase to someone else, the cost of that tangle becomes visible.
MVVM + Coordinator is not a silver bullet. But applied deliberately, it gives you two things that matter: testable business logic (in the ViewModel, away from UIKit) and navigation you can reason about (in the Coordinator, away from individual screens).
What actually moved the needle
The single biggest win was pulling network calls and state mutations out of viewDidLoad. Once the ViewModel owns that logic, you can instantiate it in a test without touching a storyboard.
The Coordinator pattern solved a subtler problem: screens that needed to navigate to different destinations depending on context. Instead of passing delegate callbacks or singletons around, each screen just calls coordinator.handle(.someEvent) and the Coordinator decides what happens next.
What was just ceremony
Protocols for every ViewModel. I’ve seen codebases with a HomeViewModelProtocol, a ProfileViewModelProtocol, and so on — one conformance each, never substituted. That’s interface overhead with no payoff. Keep protocols where you actually need substitution (networking, persistence).
Coordinator hierarchies three levels deep. Start flat. Add hierarchy when you have a genuine sub-flow (onboarding, checkout) that needs its own lifecycle.
The migration sequence that worked
- Identify one screen with complex state — not the simplest, not the most complex.
- Extract its data-fetching and state logic into a plain Swift class (
ViewModel). No protocols yet. - Make the view controller dumb: it observes the ViewModel and calls methods on it. Nothing else.
- Write one unit test for the ViewModel. If you can’t, the extraction isn’t complete.
- Repeat for the next screen. Introduce a Coordinator only when you need to move navigation logic out of a view controller.
The key is not doing it all at once. A codebase that’s 30% MVVM and 70% MVC ships. A codebase that’s 0% MVVM because the refactor was never finished doesn’t.