Template: UIKit MVP Screen
A programmatic UIKit screen built on the four-file MVP convention — Contract, View, View Controller, Presenter — plus a complete presenter test file. This is the corrected pattern: every dependency is injected through an initializer, and no work happens in init. See ../../../skills/architecture/ios/mvp.md.
Folder Structure
uikit_mvp_screen/
├── ArticleListContract.swift // view + presenter protocols, and the coordinator delegate
├── ArticleListView.swift // UIView subclass owning all layout and subviews
├── ArticleListViewController.swift // lifecycle, presenter wiring, navigation triggers
├── ArticleListPresenter.swift // all presentation logic and state; no `import UIKit`
└── ArticleListPresenterTests.swift // presenter tests: spy view + stub repositoryThe files use a sample
Article/ArticleListfeature. RenameArticle/ArticleListto your real domain entity and screen name when you copy this bundle into your codebase.
How the pieces connect
ArticleListCoordinator → ArticleListViewController.make(articles:delegate:)
│
ArticleListView (layout) ArticleListPresenter (logic)
│ │
└──── ArticleListViewProtocol / ArticleListPresenterProtocol ────┘
│
ArticleRepository (protocol)ArticleListViewController.make(articles:delegate:) is the only supported construction path: it builds the view controller, then builds the presenter with a reference to that view controller, then returns the fully wired screen. A coordinator is the only caller of make and the only conformer of ArticleListCoordinatorDelegate — see ../../../skills/architecture/ios/coordinator_navigation.md.
Conventions Demonstrated
- Contract declares
ArticleListViewProtocol,ArticleListPresenterProtocol, andArticleListCoordinatorDelegate— protocols only, no implementation. - View (
ArticleListView) owns every subview and every constraint, built once with UIKit's own layout anchors (no third-party layout library) — see../../../skills/ui/ios/uikit_view_layer.md. - View Controller owns lifecycle, presenter wiring, and navigation triggers only, and uses a plain
UITableViewDataSourceconformance: the contract'sreloadList()is an imperative "reload everything" command, not a diffable snapshot, so this screen has nothing to key a diff on.Articleis alreadyIdentifiable, so a screen whose contract instead exposes diff-friendly state can adoptUITableViewDiffableDataSource<Section, Article.ID>— see the diffable example in../../../skills/ui/ios/uikit_view_layer.md. - Presenter is
@MainActor final, holds the viewweak, takes every dependency throughinitas a protocol, and does no work ininit— loading starts from the explicitonViewDidLoad()entry point, andrefresh()is a behaviorally distinct entry point that forwardsrefresh: trueto the repository. - Tests cover the success path, the failure path, and the
refresh()/onViewDidLoad()distinction, using a spy view and a stub repository — no real network, noUIApplication.shared.
Assumptions
ApiProtocol(showLoading(),hideLoading(),handleApiError(error:)) andPresenterProtocol(empty,@MainActor) are assumed to already exist in the host app and are shared across every MVP screen — this bundle references them but does not declare them.ArticleRepository(func latest(refresh: Bool) async throws -> [Article]) is the Domain protocol defined in../../../skills/architecture/ios/repository_pattern.md— it is referenced here, never redeclared.Article.fixture()in the test file is expected from the host app's own test helpers.
Usage
- Copy the five files into your feature module.
- Rename
Article/ArticleListthroughout to your real domain entity and screen name. - Point the injected
ArticleRepositoryat your real Data-layer implementation. - Wire a coordinator that conforms to
ArticleListCoordinatorDelegateand callsArticleListViewController.make(articles:delegate:)— see../../../skills/architecture/ios/coordinator_navigation.md. - Run
ArticleListPresenterTests.swiftas-is onceArticle.fixture()resolves in your test target, and extend it with coverage for your screen's own presentation rules.
These
.swiftfiles are illustrative, not part of a compiled package — this folder is not an Xcode/SwiftPM target. Cross-file symbols (Article,ApiProtocol,PresenterProtocol,Article.fixture()) will not resolve if you try to build this folder in isolation; they resolve once the files are copied into a host app that already supplies those types.