Architecture: Organizing an Objective-C App at the Structural Level

Understand the major architectural patterns used in Objective-C/UIKit apps — MVC, MVVM, MVP, VIPER, and Clean Architecture — along with the Coordinator pattern, modular architecture, dependency inversion, feature-module organization, the service layer, and the repository pattern, and be able to reason about the trade-offs between them rather than reach for a single 'correct' answer.

Everything this course has covered so far — classes, protocols, memory management, blocks, the runtime, persistence, concurrency, testing — is about how to write a single piece of Objective-C code correctly. Architecture is a different question entirely: given hundreds or thousands of correctly written classes, how do you arrange them so the app as a whole stays understandable, testable, and changeable as it grows? A codebase where every individual method is well-written can still be a nightmare to work in if the overall structure is wrong — if there's no consistent answer to 'where does this piece of logic belong', if every change to one screen risks breaking three others, or if testing anything requires standing up half the app first. This lesson surveys the architectural patterns an Objective-C/UIKit developer is likely to encounter or choose between — starting with Apple's own default, MVC, and its well-known failure mode, then working outward through MVVM, MVP, VIPER, and Clean Architecture, before covering several cross-cutting concerns — the Coordinator pattern, modular architecture, dependency inversion, feature modules, the service layer, and the repository pattern — that apply somewhat independently of which top-level pattern a team picks. The recurring theme, and the note this lesson closes on deliberately, is that none of these is a universally correct choice; each is a trade-off, and picking well means understanding what each one is trading away.

MVC: Apple's Traditional Default, and the Massive View Controller Problem

Model-View-Controller is the architecture UIKit is built around and the one every Objective-C developer meets first, usually without consciously choosing it — it's simply what you get by subclassing UIViewController and filling in its lifecycle methods the way Xcode's templates encourage. The idea, in its textbook form, is a clean three-way split: Models hold data and business logic with no awareness of the UI at all; Views display things on screen and forward user interaction upward, with no awareness of what that interaction means; and Controllers sit in between, mediating — pulling data out of models to configure views, and translating view events (a tap, a text change) into changes to the model. On paper it's a clear, well-understood separation of concerns, and for a small screen with a handful of pieces of state, it genuinely works exactly as advertised.

The trouble is what happens as a screen grows, and it's common enough in the Objective-C/UIKit world to have its own name: the Massive View Controller problem. UIKit's UIViewController is not really 'just' the C in MVC — it's simultaneously the view lifecycle owner, the UITableViewDataSource, the UITableViewDelegate, the target for every button's action, the destination for network callbacks, the thing that owns Auto Layout constraints, and often the place where validation, formatting, and navigation logic all get written because there's nowhere more obviously convenient to put them. None of those responsibilities individually violates MVC — a controller genuinely does mediate between model and view — but stacked together in one class, across dozens of methods, a view controller quietly accumulates thousands of lines and half a dozen unrelated jobs, until it is simultaneously the hardest class in the app to read, the hardest to test (because so much of its logic is entangled with UIView and UIViewController lifecycle machinery that's awkward to instantiate in a unit test), and the riskiest to change, because nobody can be fully confident which of its many responsibilities a given edit might disturb.

MVC is not a bad architecture so much as an architecture that UIKit makes unusually easy to erode. Every pattern covered for the rest of this lesson can reasonably be understood as a response to the Massive View Controller problem specifically — a way of pulling one or more of those accumulated responsibilities (presentation logic, navigation, data access, business rules) out of the view controller and into a class built to hold just that one job, so the view controller itself can shrink back down to something closer to its original, narrow mediating role.

MVVM and MVP: Extracting Presentation Logic Into a Testable Layer

Model-View-ViewModel keeps MVC's model and view largely as they are, but interposes a new object, the ViewModel, between the view controller and the model. The ViewModel's job is presentation logic: taking raw model data and shaping it into exactly the form the view needs to display — formatting a raw NSDate into a display string, deciding whether a 'Save' button should be enabled based on form validity, computing a derived label like '3 items selected' from an underlying selection set. Crucially, a ViewModel has no reference to UIView, UIViewController, or any other UIKit class at all; it exposes plain properties and, often, a block-based or KVO-observable way for the view controller to be notified when those properties change, and the view controller's job shrinks down to almost nothing: read a property, set it on a label; observe a change, update a control's enabled state.

The payoff is testability, and it's a direct, mechanical payoff rather than a vague promise. Because a ViewModel holds no UIKit references, it can be instantiated directly in an XCTestCase — the kind of unit test this course covered in the Testing and Mocking/Dependency Injection lessons — with no need to load a storyboard, push a view controller onto a window, or otherwise wrestle with UIKit's view lifecycle just to exercise a piece of logic. A rule like 'the save button is disabled until both fields are non-empty' becomes a plain assertion against a ViewModel property, run in milliseconds, rather than logic buried inside a view controller's textFieldDidChange: that can only really be exercised by driving the actual UI. The cost is real too: MVVM adds an entire additional class per screen, plus some mechanism — blocks, KVO, or a lightweight binding helper — for the view controller to react to ViewModel changes, and Objective-C has no first-class, compiler-checked binding syntax the way some other UI frameworks do, so that wiring is usually handwritten and adds a small amount of boilerplate to every screen. For a genuinely trivial screen with little presentation logic to extract, MVVM's extra layer can be more ceremony than the screen warrants; its value scales with how much non-trivial formatting, validation, and derived state a screen actually has to manage.

Model-View-Presenter is structurally very close to MVVM and the two are often confused, but the distinction is worth being precise about because it changes how testing and updates work. In MVP, the Presenter — playing a role analogous to the ViewModel — does not expose passive properties for the view to read; instead it actively pushes updates by calling methods on a narrow view protocol the view controller implements, something like - (void)displayUserName:(NSString *)name; or - (void)showValidationError:(NSString *)message;. The view controller in MVP is thinner still than in MVVM: it implements that protocol and does essentially nothing but forward each call directly into a UIKit property set, while the Presenter owns essentially all of the logic about what should be displayed and when.

The practical difference comes down to the direction of communication and what gets tested. MVVM's view controller pulls state from the ViewModel (reading properties, observing changes); MVP's Presenter pushes state into the view controller (calling protocol methods it implements). Testing an MVP Presenter usually means supplying a mock object conforming to the view protocol and asserting that the right methods were called with the right arguments — a pattern this course's Mocking and Dependency Injection lesson covered in general form — whereas testing an MVVM ViewModel means asserting against its exposed property values directly, which many developers find slightly more natural to write and read. Neither is meaningfully more powerful than the other; the choice between them in an Objective-C codebase is largely a matter of team convention and which style of test assertions the team finds more natural, and it's common to see the two names used almost interchangeably in less rigorous descriptions of a given app's architecture.

VIPER: Heavy Layering for Strict Separation of Concerns

VIPER pushes the same idea — pull responsibilities out of the view controller — much further than MVVM or MVP do, splitting a single screen's logic across five distinct roles: the View displays UI and forwards user actions, with essentially zero logic of its own; the Interactor holds the screen's business logic, entirely independent of UI, fetching and manipulating data as the use case requires; the Presenter formats data from the Interactor for the View and translates View events back into Interactor calls, acting as a thin coordinator between the two; the Entity is a plain data/model object with no behavior; and the Router (sometimes called the Wireframe) owns navigation, deciding which screen to present next and how to configure it. Every one of these five pieces is typically its own class, often with its own protocol defining the boundary between it and its neighbors, so a single VIPER 'module' for one screen can easily comprise five to eight files.

The appeal for large teams and large apps is strict, enforced separation: because each role is a separate class behind a separate protocol, it becomes very difficult for responsibilities to blur back together the way they do in an eroding MVC view controller — there is no ambiguity about where navigation logic goes (the Router) versus business logic (the Interactor) versus display formatting (the Presenter), because each has exactly one designated home and nowhere else to hide. On a team with many engineers working across many screens simultaneously, that rigidity is a genuine asset: two engineers on the same screen are less likely to collide, code review is more predictable because everyone already knows which file a given kind of change belongs in, and unit testing each layer in isolation (mocking its neighbors behind their protocols) is very natural.

The cost is substantial and worth stating plainly: VIPER's boilerplate-per-feature is significant, even for a screen that's genuinely simple. A one-field form that would be a single, modestly-sized view controller under MVC can easily balloon into five files and several protocols under VIPER, and that overhead is paid on every screen regardless of how much complexity the screen actually has. VIPER tends to be a good fit specifically for large teams building large, long-lived apps where the discipline it forces is worth the ceremony; it tends to be a poor fit for a small team, a small app, or a screen genuinely simple enough that MVC or MVVM would handle it with a fraction of the files.

Clean Architecture: The Philosophy VIPER Draws From

Clean Architecture is broader than VIPER — it's not a specific five-role recipe for a single screen, but an architectural philosophy about how an entire app's code should be layered, and VIPER can reasonably be understood as one concrete way of applying it to UIKit screens specifically. The central image is a set of concentric layers, business logic (often called entities and use cases) at the innermost core, with UI, frameworks, networking, and database code forming outer layers around it — and the rule that makes the whole thing work is that dependencies point inward only: outer layers may know about and depend on inner layers, but inner layers must never depend on outer ones. The business logic at the center has no idea UIKit exists, no idea whether data came from NSURLSession or Core Data, and no idea what a UIViewController is.

The practical benefit of that inward-only dependency rule is isolation from change and isolation for testing. If the business rule for how a discount is calculated lives at the core, with no dependency on UIKit or on any specific networking or persistence mechanism, then swapping the UI framework, changing from Core Data to a plain SQLite store, or replacing NSURLSession with a different networking layer touches only the outer layers — the core business logic is untouched, and can be unit tested in complete isolation from all of that outer machinery, using exactly the same testing techniques this course's Testing lesson covered.

Achieving that inward-only rule mechanically, in Objective-C, is where dependency inversion (covered later in this lesson) becomes essential rather than optional: the core layer defines protocols describing what it needs (a way to fetch a user, a way to persist a record), and outer layers provide concrete classes conforming to those protocols, so the dependency arrow — in terms of which code literally references which — points inward even though the actual control flow, at runtime, crosses outward to reach a real network or database implementation. VIPER's Interactor role is a recognizable, screen-scoped instance of this same idea: the piece of a VIPER module doing business logic is, in spirit, Clean Architecture's core layer applied to one feature.

Coordinator Pattern: Extracting Navigation Out of View Controllers

One specific responsibility that piles onto view controllers under plain MVC is navigation: deciding which screen comes next, constructing and configuring it, and pushing or presenting it, often scattered across prepareForSegue:sender: overrides or ad hoc pushViewController:animated: calls buried in button handlers. The Coordinator pattern pulls that responsibility out entirely into a dedicated coordinator object — a plain class, unrelated to UIViewController, whose job is to own a navigation flow: it decides what screen to show first, creates and configures each view controller in the flow, and reacts to that view controller's outputs (usually via a delegate protocol or a block callback) to decide what happens next, including handing off to a child coordinator for a sub-flow.

The result is that a view controller under a Coordinator architecture no longer needs to know what comes after it at all — it simply reports what happened ('user tapped save', 'user tapped cancel') to whatever is listening, typically its coordinator, and has no pushViewController: or presentViewController: calls of its own anywhere in its implementation. That has two concrete benefits: the same view controller can be reused in different flows that navigate away from it differently (a detail screen reached from search versus reached from a favorites list, each wanting different next steps), and the navigation logic for an entire multi-screen flow — say, an onboarding sequence or a checkout process — lives in one place, the coordinator, rather than being distributed implicitly across every screen's segue and button-handler code.

This section is deliberately a preview rather than the full treatment — the Coordinator pattern, alongside sixteen other named patterns, gets its own dedicated, worked-example coverage in the Design Patterns lessons that follow this one in the course. What matters here, architecturally, is where it fits: Coordinators are compatible with essentially any of MVC, MVVM, MVP, or VIPER as the pattern governing an individual screen's internals, because navigation-between-screens and structure-within-a-screen are genuinely separate concerns, and a Coordinator layer is a common, largely independent addition on top of whichever of those a team has chosen.

Modular Architecture and Feature Modules: Two Different Axes of Organization

Modular architecture is about splitting an app's source code into separately buildable, separately testable units — in Xcode terms, separate frameworks or static libraries rather than one single monolithic app target — a topic this course's earlier Modules and Frameworks lesson covered from the build-system side. Architecturally, the motivation is the same isolation-and-testability story that's run through this whole lesson: a module with a small, explicit public interface and hidden internals is far easier to reason about, build in isolation, and swap out than a chunk of code entangled by unrestricted access throughout one giant target, and splitting build boundaries also physically prevents accidental coupling — code in one module simply cannot reach into another module's internals, because the compiler won't let it.

Feature modules apply that same splitting instinct along a specific axis: organize by *feature* — a 'Login' module, a 'Checkout' module, a 'Profile' module, each containing its own view controllers, its own models, its own networking calls specific to that feature — rather than organizing by *technical layer*, which would instead group all view controllers together in one place, all networking code together in another, and all model classes in a third, regardless of which feature each belongs to. Feature-based organization tends to scale better as an app and its team grow, because most day-to-day changes are scoped to one feature, and a developer working on checkout rarely needs to open files belonging to login or profile at all — the files that change together live together.

Layer-based organization is not simply wrong, and it's worth contrasting rather than dismissing: for a small app with only a handful of screens, grouping by technical layer keeps genuinely similar code together (all the networking in one place is easy to scan for someone learning the codebase) and avoids the overhead of deciding, feature by feature, exactly where a shared utility belongs. As an app grows past a certain size, though, layer-based organization tends to produce folders with dozens of loosely related files that all happen to be 'view controllers' or 'models' but have nothing else in common, while feature-based organization keeps each feature's slice of the app cohesive and — especially when paired with the modular architecture described above — able to be built, tested, and even owned by a specific sub-team in relative isolation.

Dependency Inversion: The Principle That Makes Testable Layering Possible

Dependency inversion is the principle, not a specific pattern like MVVM or VIPER: high-level modules — the code implementing your app's actual business rules — should not depend directly on low-level modules' concrete implementation details; instead, both the high-level and low-level code should depend on a shared abstraction. In practical Objective-C terms, that abstraction is a protocol, the mechanism this course's Protocols lessons covered in depth: rather than a view controller holding a direct, concrete NetworkManager * property and calling its methods, it holds a property typed as some id<DataFetching> protocol, and a concrete class implementing that protocol is supplied to it from outside, typically at construction time.

This is exactly the mechanism the earlier Mocking and Dependency Injection lesson demonstrated as the concrete way to make code testable: a view controller depending on id<DataFetching> rather than a concrete network class can be tested by injecting a fake or mock object conforming to that same protocol, with no real network call ever happening, and no change needed to the view controller's code to make that substitution possible — the protocol boundary is what makes the swap trivial. Dependency inversion and dependency injection are closely related but distinct: inversion is the architectural principle (depend on abstractions, not concretions), and injection is the mechanical technique (supplying the concrete implementation from outside rather than having a class construct its own dependencies internally) that typically satisfies that principle in practice.

The reason dependency inversion matters at the architecture level, beyond individual testability, is that it's precisely what allows Clean Architecture's inward-only dependency rule to hold in a language like Objective-C that has no built-in mechanism for enforcing it structurally: the core business-logic layer defines the protocols it needs, outer layers supply conforming implementations, and the core layer's .h/.m files never need to #import anything from those outer layers at all. Without protocol-based dependency inversion, achieving genuine inward-only dependencies in Objective-C would require awkward workarounds; with it, the principle becomes a straightforward, everyday application of a tool the language already provides well.

Service Layer and Repository Pattern

The service layer is a dedicated layer of classes encapsulating business logic and coordination with external systems, sitting conceptually between the UI (view controllers, ViewModels, Presenters — whichever pattern a team uses) and raw data access. A typical service class — say, AuthenticationService or CheckoutService — exposes a small number of high-level operations (- (void)loginWithUsername:...password:...completion:...;) and internally coordinates whatever's actually required to fulfill that operation: making one or more network calls, validating a response, updating local storage, and posting a notification when it's done, all details the caller (a view controller, most often) doesn't need to know about. Putting this coordination in a service class rather than directly in a view controller is, again, a direct application of the same idea that's run through this whole lesson: it pulls a non-trivial chunk of logic out of the view controller and into a class that can be tested and reused independently of any specific screen.

The repository pattern is a narrower, more specific idea focused purely on data access: a repository is a protocol-typed abstraction — something like id<UserRepository> declaring methods such as - (void)fetchUserWithID:...completion:...; and - (void)saveUser:...completion:...; — that hides from its caller exactly where the data actually comes from. A concrete class conforming to that protocol might fetch from a network endpoint, read from a local Core Data store, check an in-memory cache first, or some combination of all three, and the caller — typically a service or a ViewModel — genuinely does not need to know or care which, because it only ever talks to the protocol. This is the same architectural benefit this course's earlier Persistence-family lessons hinted at when discussing choosing between UserDefaults, the Keychain, SQLite, and Core Data: a repository is the formal pattern for making that storage choice swappable and hidden from the rest of the app, so a change from, say, a network-only data source to a network-with-local-cache data source touches only the repository's concrete implementation, not any of its callers.

Service layer and repository pattern are often used together and are easy to conflate, but they answer different questions: a repository answers 'where does this data live and how do I get it', while a service answers 'what business operation am I performing, and what steps does it require'. A checkout service, for instance, might depend on both an id<OrderRepository> to persist the order and an id<PaymentGateway> to charge a card, coordinating between the two to fulfill a single high-level 'place order' operation — the service orchestrates, and the repositories (and other protocol-typed dependencies) it holds each handle one narrow, swappable piece of that orchestration.

There Is No Single Best Architecture

It would be convenient if this lesson ended by naming one of MVC, MVVM, MVP, VIPER, or Clean Architecture as simply the correct choice, but that would be a disservice, and it's also explicitly not what real Objective-C/Cocoa engineering practice supports. Every pattern surveyed in this lesson is a trade-off, and the trade-off it makes is real in both directions: MVC is fast to start with and has essentially zero ceremony, at the cost of degrading into Massive View Controllers as a screen grows; MVVM and MVP add a testable presentation layer at the cost of an extra class and some hand-wired update mechanism per screen; VIPER enforces very strict separation at the cost of significant boilerplate on every single screen, simple or complex alike; Clean Architecture's inward-only dependency discipline buys isolation from framework and UI churn at the cost of more protocols, more indirection, and more files to navigate when reading the code.

The right choice for a given app depends on concrete, situational factors rather than an abstract notion of 'best': team size (a large team benefits more from VIPER's rigid, collision-resistant boundaries than a two-person team ever will), app complexity (a five-screen utility app rarely justifies VIPER or full Clean Architecture layering at all), how much automated testing the team actually intends to write and rely on (a team with a strong testing culture gets real, ongoing value from MVVM's or MVP's testable presentation layer; a team that doesn't write many unit tests gets comparatively little from that same investment), and, frankly, how much boilerplate-per-screen a team is willing to maintain and review, indefinitely, for the lifetime of the app.

The habit worth building, then, is not memorizing a ranking of these patterns but learning to ask the right questions when a team is choosing or evolving one: how bad is our Massive View Controller problem actually getting, right now, on real screens — not hypothetically? How much of our logic genuinely needs unit-test coverage, and how much of that is even reachable today? Is our team's current pain point testability, navigation sprawl, or onion-like coupling to frameworks — because different pains point toward different fixes, MVVM/MVP for the first, Coordinators for the second, Clean Architecture-style dependency inversion for the third. An architecture chosen by reasoning through those trade-offs for the app actually in front of you will consistently outperform one chosen because it's fashionable or because a blog post declared it 'best'.

The Shape of Dependency Inversion via a Protocol Boundary

ProfileViewModel never imports NetworkUserRepository.h -- it only knows about the UserRepository protocol, which is exactly the inward-only dependency direction Clean Architecture and dependency inversion both require. Injecting the concrete repository through the initializer (rather than ProfileViewModel constructing its own) is what makes a mock or fake id<UserRepository> easy to substitute in a unit test, per the Mocking and Dependency Injection lesson. The same protocol-boundary shape underlies the service layer, the repository pattern, and every one of MVVM/MVP/VIPER's inter-layer boundaries -- it is the one recurring mechanical idea beneath most of this lesson's named patterns.

// The protocol both sides depend on -- this is the "abstraction" in dependency inversion:
@protocol UserRepository <NSObject>
- (void)fetchUserWithID:(NSString *)userID
              completion:(void (^)(User *_Nullable user, NSError *_Nullable error))completion;
@end

// High-level code (a service, or a ViewModel) depends only on the protocol, never a concrete class:
@interface ProfileViewModel : NSObject
- (instancetype)initWithUserRepository:(id<UserRepository>)repository; // injected from outside
@end

// A concrete, low-level implementation is supplied later, at composition time --
// swapping it (network-backed today, cache-backed tomorrow) never touches ProfileViewModel at all.
@interface NetworkUserRepository : NSObject <UserRepository>
@end

Code Examples

Every one of these responsibilities -- table data source, text field validation, raw networking, formatting-for-display, navigation -- is individually a reasonable thing for *some* class to own. The problem is that all of them have piled onto one UIViewController subclass with no boundaries between them, which is exactly what the rest of this lesson's patterns exist to prevent: MVVM would pull the validation and enable/disable logic into a ViewModel, a repository would own the networking, and a Coordinator would own the navigation push, leaving the view controller itself to do far less.

@interface ProfileViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, NSURLSessionDelegate>

Common mistake: Treating every screen-related responsibility as automatically belonging on the view controller simply because it's the class that's already there and convenient to add one more method to.

The validity rule for the Save button -- non-empty, no more than 160 characters -- now lives entirely inside ProfileViewModel, a plain NSObject subclass with zero UIKit imports. It can be unit tested directly: construct one, call -updateBioText: with various strings, and assert on isSaveEnabled, with no view controller, no window, and no storyboard involved at all. The view controller's only remaining job for this piece of logic is to set saveButton.enabled = viewModel.isSaveEnabled; inside the _didChangeHandler block.

return _bioText.length > 0 && _bioText.length <= 160;

CachingArticleRepository itself conforms to id<ArticleRepository>, the same protocol its own networkRepository property is typed as -- a repository can wrap another repository transparently. A ViewModel or service holding an id<ArticleRepository> property calls exactly the same method, -fetchArticlesWithCompletion:, whether it's actually talking to this caching decorator, a plain network-only implementation, or (in a unit test) a fake that returns canned data synchronously; none of that is visible at the call site. This is also a preview of the Decorator pattern from the Design Patterns lessons that follow: CachingArticleRepository wraps another object conforming to the same interface and adds behavior (caching) without the caller needing to know it's wrapped at all.

if (self.cachedArticles) { completion(self.cachedArticles, nil); return; }

@interface ProfileViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, NSURLSessionDelegate>
@end

@implementation ProfileViewController
// ... view lifecycle, Auto Layout setup ...
// ... UITableViewDataSource / UITableViewDelegate methods for a table of settings rows ...
// ... UITextFieldDelegate methods validating a bio text field character-by-character ...
// ... an NSURLSession call fetching the user's profile, its completion handler parsing JSON
//     and reaching directly into table view cells to update them ...
// ... business logic deciding whether the "Save" button should be enabled, inline in
//     -textField:shouldChangeCharactersInRange:replacementString: ...
// ... navigation: -tableView:didSelectRowAtIndexPath: directly pushes the next screen ...
@end
@interface ProfileViewModel : NSObject
@property (nonatomic, copy, readonly) NSString *bioText;
@property (nonatomic, readonly) BOOL isSaveEnabled;
- (void)updateBioText:(NSString *)text;
- (void)setDidChangeHandler:(void (^)(void))handler; // notifies the view controller of updates
@end

@implementation ProfileViewModel {
    void (^_didChangeHandler)(void);
}

- (void)updateBioText:(NSString *)text {
    _bioText = [text copy];
    [self willChangeValueForKey:@"isSaveEnabled"];
    [self didChangeValueForKey:@"isSaveEnabled"];
    if (_didChangeHandler) { _didChangeHandler(); }
}

- (BOOL)isSaveEnabled {
    return _bioText.length > 0 && _bioText.length <= 160;
}

@end
@protocol ArticleRepository <NSObject>
- (void)fetchArticlesWithCompletion:(void (^)(NSArray<Article *> *articles, NSError *error))completion;
@end

@interface CachingArticleRepository : NSObject <ArticleRepository>
@property (nonatomic, strong) id<ArticleRepository> networkRepository;
@property (nonatomic, strong) NSArray<Article *> *cachedArticles;
@end

@implementation CachingArticleRepository

- (void)fetchArticlesWithCompletion:(void (^)(NSArray<Article *> *, NSError *))completion {
    if (self.cachedArticles) {
        completion(self.cachedArticles, nil); // return the cache instantly, no network call at all
        return;
    }
    [self.networkRepository fetchArticlesWithCompletion:^(NSArray<Article *> *articles, NSError *error) {
        if (articles) { self.cachedArticles = articles; }
        completion(articles, error);
    }];
}

@end

Where Responsibilities Live, MVC vs. MVVM+Coordinator+Repository

The total amount of work is roughly the same in both diagrams -- what changes is whether it's concentrated in one class or distributed across classes each built to hold one kind of responsibility.

Plain MVC (everything piles onto the controller)
  +----------------------------------------------------+
  |                UIViewController                    |
  |  data source | delegate | validation | networking   |
  |  formatting  | navigation | business rules           |
  +----------------------------------------------------+

  MVVM + Coordinator + Repository (each responsibility has one home)
  +---------------+   +----------------+   +--------------------+
  |  Coordinator  |-->| UIViewController|<->|   ViewModel        |
  | (navigation)  |   | (display only)  |   | (presentation logic)|
  +---------------+   +----------------+   +----------+---------+
                                                        |
                                                        v
                                             +----------------------+
                                             | id<ArticleRepository> |
                                             |  (data access, hidden |
                                             |   behind a protocol)  |
                                             +----------------------+

Real-World Note: Most Production Apps Mix Patterns

It's rare, in practice, to find a large Objective-C app that is purely and consistently one of these named patterns end to end. A more common real-world shape is MVC or MVVM for most ordinary screens, a Coordinator layer running underneath all of them handling navigation, repositories behind the newer or more actively developed features (where testability was worth investing in), and perhaps one or two legacy screens still doing direct, old-style Massive-View-Controller-flavored networking that nobody has had the time or the justification to refactor yet. Recognizing that this kind of pragmatic mixture is normal -- rather than assuming a codebase must commit to exactly one pattern everywhere -- is itself a useful piece of architectural judgment, and often matches how a real codebase evolves incrementally over years rather than being designed once from a blank slate.

Adopting VIPER for every screen in a small, three-person team's simple utility app because it's the architecture a conference talk recommended.

Match the architecture's ceremony to the app's actual complexity and the team's actual size -- a small app with a small team often does perfectly well with MVC or lightweight MVVM, and VIPER's five-files-per-screen overhead becomes pure friction with no compensating benefit.

VIPER's strict separation earns its keep specifically on large teams and large apps where collision-avoidance and enforced boundaries matter; imposed on a small, simple codebase, it mostly just multiplies the number of files a developer has to open to make a one-line change.

Letting a view controller keep growing indefinitely -- adding 'just one more' data source method, delegate conformance, or inline networking call -- because it's already the class that's there.

Treat a view controller's growing responsibility list as a signal to extract, not accommodate -- pull validation and formatting into a ViewModel or Presenter, pull navigation into a Coordinator, and pull data access behind a repository, before the class becomes unmanageable.

The Massive View Controller problem doesn't arrive suddenly; it accumulates one small, individually reasonable addition at a time, which is exactly why it's worth recognizing and interrupting early rather than waiting for the class to become obviously unmanageable.

Having a core business-logic class #import a concrete networking or persistence class directly, then calling it 'Clean Architecture' because the folders are named Entities, UseCases, and Frameworks.

Enforce the inward-only dependency rule mechanically, with protocols the core layer defines and outer layers conform to -- folder names alone do not create the isolation Clean Architecture is actually for.

Clean Architecture's entire benefit -- testing business logic without framework or UI dependencies, and swapping outer-layer implementations freely -- depends on the dependency direction being real, not just organized to look that way; a stray #import of a concrete outer-layer class breaks the isolation regardless of which folder the importing file lives in.

Choosing an architecture once, at a project's very beginning, and treating it as permanently fixed regardless of how the app's actual pain points evolve over the following years.

Revisit the choice as the app and team change -- a five-screen MVC prototype that grows into a forty-screen app with a growing team is a reasonable candidate to gradually introduce MVVM, Coordinators, or repositories screen by screen, rather than staying locked into whatever fit the original, much smaller app.

The trade-offs this lesson describes are not static; a team of two choosing MVC for a prototype is making a good decision for that moment, and the same team, grown to fifteen engineers on the same codebase two years later, is very often making a bad decision by refusing to reconsider it.

What is the 'Massive View Controller' problem specifically a consequence of? A. A bug in UIKit's UIViewController class that Apple has never fixed B. UIKit making it unusually easy for a view controller to accumulate many unrelated responsibilities (data source, delegate, networking, formatting, navigation) with nothing structurally preventing it C. Using Swift instead of Objective-C for a project's view controllers D. Storyboards being deprecated in favor of programmatic UI

B. UIKit making it unusually easy for a view controller to accumulate many unrelated responsibilities (data source, delegate, networking, formatting, navigation) with nothing structurally preventing it — UIKit's UIViewController is simultaneously the natural home for the view lifecycle, table view data source/delegate conformance, target-action handlers, and more, with nothing in the framework preventing all of that from piling onto one class -- which is exactly what erodes plain MVC over time.

What is the key structural difference between MVVM and MVP as covered in this lesson? A. MVP does not use Objective-C protocols at all, while MVVM requires them B. MVVM's view controller pulls state from ViewModel properties (often via KVO or blocks); MVP's Presenter actively pushes updates by calling methods on a view protocol the view controller implements C. MVP is only usable with Swift, and MVVM is only usable with Objective-C D. There is no real difference; the two terms are always fully interchangeable in every codebase

B. MVVM's view controller pulls state from ViewModel properties (often via KVO or blocks); MVP's Presenter actively pushes updates by calling methods on a view protocol the view controller implements — MVVM communication is typically pull-based (the view controller reads/observes ViewModel properties); MVP communication is typically push-based (the Presenter calls methods on a view protocol). The distinction affects how each is tested but neither is fundamentally more capable than the other.

Why does VIPER tend to be a poor fit for a small app built by a small team? A. VIPER cannot be implemented in Objective-C, only in Swift B. VIPER's per-screen boilerplate (typically five or more classes/protocols per screen) is paid regardless of a screen's actual complexity, and its main benefit -- collision-resistant boundaries for many engineers working simultaneously -- has little value with few engineers C. VIPER is strictly slower at runtime than MVC D. VIPER cannot support navigation between screens

B. VIPER's per-screen boilerplate (typically five or more classes/protocols per screen) is paid regardless of a screen's actual complexity, and its main benefit -- collision-resistant boundaries for many engineers working simultaneously -- has little value with few engineers — VIPER's strict five-role split earns its cost specifically when many engineers are working across many screens and need enforced boundaries; a small team on a small app pays the same per-screen boilerplate without getting much benefit from the rigidity it buys.

In Clean Architecture's concentric-layers model, what rule governs how dependencies may point between layers? A. Dependencies may point in either direction as long as the layers are documented B. Outer layers (UI, frameworks, networking, database) may depend on inner layers (business logic), but inner layers must never depend on outer layers C. Only the innermost layer may depend on anything else in the app D. There is no dependency rule; Clean Architecture is defined entirely by folder naming conventions

B. Outer layers (UI, frameworks, networking, database) may depend on inner layers (business logic), but inner layers must never depend on outer layers — The inward-only dependency rule is Clean Architecture's central mechanism: the business-logic core has no awareness of UIKit, specific networking classes, or specific persistence mechanisms, which is what makes it independently testable and lets outer-layer implementations be swapped freely.

What concrete Objective-C mechanism does dependency inversion rely on to let high-level code avoid depending on low-level concrete classes? A. Method swizzling performed at app launch B. Protocols -- high-level code depends on a protocol type (an abstraction), and a concrete low-level class is supplied, conforming to that protocol, typically injected from outside C. The #import preprocessor directive, used consistently across every file D. Storing all dependencies as global variables accessible from anywhere

B. Protocols -- high-level code depends on a protocol type (an abstraction), and a concrete low-level class is supplied, conforming to that protocol, typically injected from outside — Protocols are the abstraction dependency inversion depends on in Objective-C: a class holds a property typed as a protocol rather than a concrete class, and a concrete conforming implementation is supplied from outside -- the same mechanism the Mocking and Dependency Injection lesson used to make code testable.

Per this lesson's closing section, how should a team decide between MVC, MVVM, MVP, VIPER, and Clean Architecture? A. Always choose VIPER, since it is objectively the most rigorous and therefore the best architecture for any app B. Always choose plain MVC, since it is what Apple's own templates use by default and therefore the officially sanctioned choice C. Weigh concrete, situational trade-offs -- team size, app complexity, how much automated testing the team actually relies on, and how much per-screen boilerplate the team is willing to maintain -- since no single architecture is universally best D. The choice does not matter at all, since all of these patterns produce functionally identical code once compiled

C. Weigh concrete, situational trade-offs -- team size, app complexity, how much automated testing the team actually relies on, and how much per-screen boilerplate the team is willing to maintain -- since no single architecture is universally best — The lesson's explicit conclusion is that there is no universal best architecture; the right choice depends on team size, app complexity, testing needs, and the team's tolerance for per-screen boilerplate, and picking well means reasoning through those trade-offs for the specific app in front of you.

You've inherited a 2,000-line UIViewController that acts as its own table view data source and delegate, makes direct NSURLSession calls in a button handler, formats dates and currency inline wherever they're displayed, and pushes the next screen directly from -tableView:didSelectRowAtIndexPath:. Propose a concrete refactoring plan: which pieces of responsibility would you extract, into what kind of class each, and in what order would you tackle them to keep the app working at every intermediate step?

A reasonable order: (1) Extract navigation into a Coordinator -- replace the direct pushViewController: call with the view controller reporting 'row selected' to a delegate/coordinator, which is a small, low-risk, mechanical change. (2) Extract networking behind an id<ArticleRepository>-style protocol, injected into the view controller, so the raw NSURLSession call and its JSON parsing move out entirely -- this also unlocks testing the screen with a fake repository. (3) Extract the date/currency formatting and any derived display state into a ViewModel, leaving the view controller to read ViewModel properties and set them directly on labels. (4) Leave the UITableViewDataSource/UITableViewDelegate conformance in place initially (it's reasonable for the view controller to keep it, or it could later move to a separate data-source object) -- the goal isn't necessarily VIPER-level splitting, just breaking the three riskiest entangled responsibilities (navigation, networking, formatting) out into testable, single-purpose classes.

Consider extracting in order of risk and payoff: navigation into a Coordinator first (usually mechanical and low-risk), then networking behind a repository protocol, then presentation/formatting logic into a ViewModel or Presenter -- each extraction should leave the app fully working before starting the next.