← All topics/Architecture & design patterns

Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

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 through objectWillChange; @Observable (Observation) is a class macro that synthesizes change tracking for stored properties.
UIKit — VM protocol, VC stays thin
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
}
SwiftUI — state in model, not in View
@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.