How MVP+Context Transforms iOS Architecture for Scalable Apps
This article analyzes common iOS architectural patterns, identifies the drawbacks of traditional MVP implementations, and introduces a deep‑optimization MVP+Context solution with role definitions, automatic binding, memory‑leak prevention, and Swift support, illustrated with real‑world code and diagrams.
Background Introduction
In many iOS projects we encounter MVC, MVP, MVVM, VIPER and other patterns. As the codebase grows, view controllers become massive, coupling increases, and testing becomes difficult. Optimizing the architecture aims to improve maintainability, testability, extensibility, reduce development cost, and deliver a better user experience.
Common Architecture Patterns
MVC
Model‑View‑Controller separates the app into three independent layers: Model (business logic and data handling), View (UI presentation), and Controller (mediates between Model and View). It works well for small projects but the controller can become bloated as complexity rises.
MVP
Model‑View‑Presenter also has three layers. The Presenter handles user interactions, fetches data from the Model, formats it, and updates the View. This reduces coupling between Model and View, but the Presenter may become too large and tightly coupled to a specific View.
MVVM
Model‑View‑ViewModel introduces a ViewModel that bridges Model and View via data binding. It simplifies UI logic but can become hard to debug and maintain when the data flow is overly complex.
VIPER
View‑Interactor‑Presenter‑Entity‑Router enforces strict separation of concerns. It is suitable for large projects but adds many layers, increasing complexity for small apps.
Traditional MVP Practice
In MVP, all communication between View and Model goes through the Presenter, which often leads to a massive Presenter class and a proliferation of interface definitions, creating “glue code” and long access chains that hinder maintainability.
Typical MVP Issues
Excessive glue code across multiple interface files.
Long hierarchical access paths (e.g., view → presenter → parent presenter → …).
Difficulty accessing shared data models from deep sub‑views.
MVP+Context Architecture Solution
Role Division
Context middleware: passes information and shares data among all Presenters and Views.
Main Controller (VC): binds Context, Presenter, View, and Interactor; manages lifecycle.
Main View: handles UI, delegates logic to Context.
Main Presenter: contains business logic, may host multiple sub‑Presenters.
Interactor: implements detailed business operations.
Architecture Flow
Every View (including sub‑views) can directly obtain the Context object, which provides access to the main View, Presenter, and Controller, enabling low‑coupling, high‑cohesion interactions.
Automatic Binding
Binding is performed in viewDidLoad of the base controller. The following Objective‑C code demonstrates the binding process:
- (id<HomePresenterInterface>)presenter {
if (!_presenter) {
_presenter = [[HomePresenter alloc] initWithView:self];
PricePresenter *pricePresenter = [[PricePresenter alloc] initWithView:self.priceView parentView:self];
_presenter.pricePresenter = pricePresenter;
_presenter.categoryPresenter = [[BusinessPresenter alloc] initWithView:self.categoryView parentView:self];
_presenter.navPresenter = [[NavBarPresenter alloc] initWithView:self.navBarView parentView:self];
}
return _presenter;
}Memory‑Leak Prevention
Context holds a weak reference to the controller to break retain cycles, ensuring that when the controller is deallocated, the Context is also released.
Middleware Everywhere
Using Objective‑C runtime association, any object can retrieve the Context by recursively searching its super‑view hierarchy. The association is weak to avoid leaks.
@implementation NSObject (HLLCT)
- (void)setContext:(HLLContext *)object {
objc_setAssociatedObject(self, @selector(context), object, OBJC_ASSOCIATION_ASSIGN);
}
- (HLLContext *)context {
id curContext = objc_getAssociatedObject(self, @selector(context));
if (curContext == nil && [self isKindOfClass:[UIView class]]) {
UIView *view = (UIView *)self;
UIView *superV = view.superview;
while (superV) {
if (superV.context) { curContext = superV.context; break; }
superV = superV.superview;
}
if (curContext) [self setContext:curContext];
}
return curContext;
}
@endSupporting Swift
Swift cannot use categories, so a protocol with a default implementation provides similar Context access. The protocol is adopted by NSObject to make the property available to all classes.
protocol HLLUContextProtocol {
var context: HLLUContext? { get set }
}
extension HLLUContextProtocol {
var context: HLLUContext? {
get {
var cur: HLLUContext? = objc_getAssociatedObject(self, &kAssociatedObjectKey) as? HLLUContext
if cur != nil { return cur }
guard let view = self as? UIView else { return nil }
var superV = view.superview
while let sv = superV {
if let ctx = sv.context { cur = ctx; break }
superV = sv.superview
}
superV?.context = cur
return cur
}
set { objc_setAssociatedObject(self, &kAssociatedObjectKey, newValue, .OBJC_ASSOCIATION_ASSIGN) }
}
}
extension NSObject: HLLUContextProtocol {}Swift Binding Example
public func startBinding(bundleName: String, prefixName: String) {
self.isMVPStructor = true
self.context = self.theContext
// Bind Presenter
if let presenterClass = NSClassFromString("(bundleName).HLLU\(prefixName)Presenter") as? HLLUBasePresenter.Type {
let presenter = presenterClass.init()
self.context?.presenter = presenter
self.context?.presenter?.context = self.context
}
// Bind Interactor
if let interactorClass = NSClassFromString("(bundleName).HLLU\(prefixName)Interactor") as? HLLUBaseInteractor.Type {
let interactor = interactorClass.init()
self.context?.interactor = interactor
self.context?.interactor?.context = self.context
}
// Bind View
if let viewClass = NSClassFromString("(bundleName).HLLU\(prefixName)View") as? HLLUBaseView.Type {
let view = viewClass.init()
self.context?.view = view
self.context?.view?.context = self.context
}
// Mutual binding
self.context?.presenter?.view = self.context?.view
self.context?.presenter?.baseController = self
self.context?.interactor?.baseController = self
self.context?.view?.presenter = self.context?.presenter
self.context?.view?.interactor = self.context?.interactor
self.view = self.context?.view
}Context‑Based Practice
Using the order‑confirmation page of the HuoLaLa app as an example, the workflow shows how Context enables direct communication between price, service, and vehicle modules without extensive glue code.
Quick MVP Template Generation
A Bash script can generate Presenter, Interactor, and View classes automatically based on a module name, reducing manual errors.
#!/bin/bash
read -p "Enter the module name: " moduleName
# Generate Presenter
presenterHeader="${moduleName}Presenter.h"
presenterImpl="${moduleName}Presenter.m"
echo "@interface ${moduleName}Presenter : NSObject" > $presenterHeader
echo "@property (nonatomic, weak) HLLContext *context;" >> $presenterHeader
echo "@end" >> $presenterHeader
echo "@implementation ${moduleName}Presenter" > $presenterImpl
echo "@end" >> $presenterImpl
# Similar code for Interactor and View omitted for brevityConclusion
The article examined the shortcomings of traditional MVP in large iOS projects and proposed a Context‑based MVP+Context architecture that simplifies binding, eliminates glue code, prevents memory leaks, and works seamlessly in both Objective‑C and Swift. Selecting the right architecture depends on project size and complexity, and the presented solution offers a practical path for scalable mobile development.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
