Workflow: Migrate a UIKit Screen to SwiftUI
Strangler-fig procedure for incrementally replacing a UIKit + MVP screen with a SwiftUI + MVVM view layer while leaving UIKit in control of navigation.
Objective
Replace one leaf screen's view layer with SwiftUI while UIKit retains ownership of navigation, ensuring the app builds, tests pass, and ships after every single step.
Inputs
- Target UIKit screen (
<Name>Contract.swift,<Name>View.swift,<Name>ViewController.swift,<Name>Presenter.swift). - Existing Domain entity and repository protocol (e.g.
ArticleRepository). - Legacy data source or singleton reference, if not yet abstracted behind a repository protocol.
Outputs
- New SwiftUI
Viewand@MainActorObservableObjectview model consuming the existing repository protocol. UIHostingControllerfactory wrapping the SwiftUI view, matching the old view controller's factory signature.- Deleted legacy UIKit four-file screen files.
- Unit tests for the new view model covering success and error states.
Step-by-Step Process
- Choose a leaf screen. The target screen must have no child screens that it pushes directly, or must push only through a coordinator (Coordinator Navigation). Screens that push successors directly must be migrated last or refactored to use a coordinator first.
- Extract the repository seam first if the presenter still calls a singleton or completion-handler API directly. Introduce an
asyncrepository protocol and adapter per Bridging PromiseKit and Completion Handlers to async/await. Do not skip this step — a SwiftUI view backed by a singleton is no more testable than the view controller it replaced. - Write the SwiftUI view and view model. Create an
@MainActorObservableObjectview model that consumes the repository protocol (the same protocol used by the MVP presenter, per MVP Architecture). Build the SwiftUIViewrendering state from this view model. - Host the SwiftUI view. Wrap the view inside a
UIHostingController. Expose a factory method (e.g.make(articles:delegate:)) on the hosting controller or a builder that matches the legacyUIViewController.make(...)signature, so all existing caller and navigation sites remain untouched. - Delete old UIKit screen files. Remove the old
<Name>Contract.swift,<Name>View.swift,<Name>ViewController.swift, and<Name>Presenter.swiftfiles in a dedicated commit once the new view layer is wired and verified. - Leave navigation in UIKit. The hosting controller continues to be pushed or presented by the host app's existing
UINavigationControlleror coordinator. Do not attempt to replace UIKit navigation withNavigationStackduring view layer migration.
Validation Steps
- Target screen renders properly inside
UIHostingController. - All existing navigation call sites function without modifications outside the target screen factory.
- Unit tests for the new view model pass cleanly.
- Build succeeds with zero warnings or dead legacy file references.
markdownlint-cli2andmarkdown-link-checkpass.
Failure Scenarios
- Target screen is not a leaf screen → Stop. Migrate its child screens first or extract navigation into a coordinator before proceeding.
- Presenter logic is shared by multiple screens → Extract the shared presentation or domain logic into a domain Use Case or shared service before migrating the view layer.
- SwiftUI view requires a complex or unavailable UIKit control → Wrap the legacy control in
UIViewRepresentablerather than abandoning the migration or forcing a full redesign.
AI Agent Instructions
- Execute steps sequentially; do not attempt big-bang migrations across navigation and view layers simultaneously.
- Preserve existing factory signatures so call sites outside the target screen remain unchanged.
- Ensure the repository seam is fully async and protocol-oriented before starting the SwiftUI view model.
- Write unit tests for the new view model before removing legacy presenter tests.
Acceptance Criteria
- [ ] Target screen factory signature matches the original legacy screen factory.
- [ ] No call site outside the target screen factory was modified.
- [ ] Legacy four-file screen assets (
Contract,View,ViewController,Presenter) are deleted. - [ ] SwiftUI View Model is
@MainActor, usesObservableObject, and has unit tests for success and failure paths. - [ ] Navigation remains owned by UIKit (
UINavigationController/ Coordinator).