Skip to content
Prepare to Grow

iOS Architecture

MVC and the Massive View Controller Problem

Use Apple's MVC responsibly by keeping controllers focused on presentation coordination instead of absorbing every concern.

Advanced6 min read2 guided activities

Before you begin

What you will understand

  • Explain Model–View–Controller roles in UIKit.
  • Identify why view controllers become massive.
  • Extract domain, data, formatting, and navigation responsibilities.
  • Recognize when a simple controller is sufficient.
01

Learning stage

Concept Explanation

In UIKit MVC, models represent data and rules, views display content and receive interaction, and view controllers coordinate between them. UIViewController is already a framework controller with lifecycle and containment responsibilities; it does not need to become the networking client, decoder, database, analytics service, formatter, and router as well.

Massive view controllers grow through convenience. A new request is started in viewDidLoad, JSON is decoded beside table code, validation appears in a button action, and navigation construction spreads across callbacks. Each addition looks small, but unrelated reasons to change accumulate in one stateful object that is difficult to test or reuse.

Refactor around responsibilities, not line count. Extract domain rules into values or use cases, data access behind a focused dependency, formatting into pure collaborators, and complex route construction into a coordinator. Keep screen-specific orchestration in the controller. A small static screen may need nothing more—architecture should reduce cognitive load, not maximize type count.

  • Model: data and rules independent from a particular rendered view.
  • View: visual hierarchy and user interaction surface.
  • Controller: coordinates lifecycle, presentation, and user intent.
  • Massive controller: object carrying unrelated change responsibilities.
  • Extraction seam: focused collaborator introduced around coherent behavior.
02

Learning stage

Syntax & Examples

A controller can remain an MVC coordinator when loading and formatting are delegated to explicit collaborators.

CourseListViewController.swiftswift
@MainActor
final class CourseListViewController: UIViewController {
  private let loader: any CourseLoading
  private let formatter: CourseRowFormatter
  private let tableView = UITableView()
  private var courses: [Course] = []

  init(loader: any CourseLoading, formatter: CourseRowFormatter) {
    self.loader = loader
    self.formatter = formatter
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) { fatalError("Use dependency initializer") }

  override func viewDidLoad() {
    super.viewDidLoad()
    configureHierarchy()
    Task { await loadCourses() }
  }

  private func loadCourses() async {
    do {
      courses = try await loader.courses()
      tableView.reloadData()
    } catch {
      presentLoadFailure(retry: { [weak self] in
        Task { await self?.loadCourses() }
      })
    }
  }
}

struct CourseRowFormatter {
  func subtitle(for course: Course) -> String {
    "(course.completedLessons) of (course.totalLessons) complete"
  }
}
The controller still coordinates UIKit, while loading and deterministic formatting have independent owners.
03

Learning stage

Real-World Usage

Connect the architectural concept to maintainable iOS features and real change scenarios.

  • Keep lifecycle and UIKit wiring in a controller while testing rules separately.
  • Move request construction and decoding behind a loader.
  • Reuse formatting in cells, widgets, or another screen.
  • Extract navigation only after the flow becomes a meaningful responsibility.
04

Learning stage

Common Mistakes

Identify abstractions and dependencies that add coupling, ambiguity, or unnecessary ceremony.

  • Calling every type ending in Manager an architectural improvement.
  • Moving controller code into an equally massive view model.
  • Letting a UIView subclass own networking and navigation.
  • Extracting one-line helpers into distant files that reduce readability.
  • Assuming MVC is inherently untestable instead of examining the actual coupling.
05

Learning stage

Practice Exercises

Apply the design to a small feature and evaluate it through observable change scenarios.

Activity 1

Audit a massive controller

Mark each method in a sample controller as lifecycle, rendering, domain rule, data access, formatting, navigation, or analytics. Group candidates for extraction.

Hint: A coherent responsibility often changes for one reason and can be named in product language.

Activity 2

Refactor one seam

Extract one deterministic rule and one external dependency while leaving screen coordination in the controller. Add focused tests.

Hint: Measure improvement through simpler tests and fewer change reasons, not only fewer lines.

06

Learning stage

Knowledge Check

Answer each question before revealing the explanation.

QUESTION 1

What is a healthy primary responsibility for a UIKit view controller?

  1. A.Coordinate its view lifecycle, rendering, and user intent
  2. B.Own every persistence table
  3. C.Implement all business rules
  4. D.Act as the global service locator
Reveal answer

Coordinate its view lifecycle, rendering, and user intent

Other responsibilities can live in focused model, service, use-case, formatting, or navigation collaborators.

QUESTION 2

What makes a view controller massive?

Reveal answer

Accumulating unrelated reasons to change and tightly coupled responsibilities, not crossing a specific line-count threshold.

A long controller with one coherent job may be clearer than many tiny indirections.

07

Learning stage

Interview Preparation

Explain the decision, dependency direction, trade-offs, and evidence—not only the pattern name.

How would you refactor a massive view controller?

Short answer: Map responsibilities, extract the highest-value seams incrementally, and keep UIKit coordination local.

Explore the detailed answer

I first add characterization tests around risky behavior, then identify domain decisions, external I/O, formatting, and navigation. I extract narrow collaborators with explicit inputs, outputs, and lifetimes, inject them, and verify the controller now translates state into UI. I avoid a wholesale rewrite or an equally massive replacement type.

Possible follow-ups

  • • Would you always replace MVC with MVVM?
  • • Which responsibility would you extract first?
08

Learning stage

Key Takeaways

MVC works when its roles stay meaningful. The problem is not UIViewController itself but the gradual accumulation of unrelated policy, I/O, formatting, and routing.

  • UIKit controllers already own lifecycle responsibilities.
  • Massive means many reasons to change.
  • Extract coherent behavior, not arbitrary lines.
  • Keep external I/O behind injected dependencies.
  • Simple screens deserve simple architecture.