Skip to main content
3Nsofts logo3Nsofts
iOS Architecture

Modernizing a Legacy iOS App: Refactor, Add SwiftUI, or Rewrite?

Choose a modernization path using release risk, data ownership, dependencies, and measurable product outcomes. Includes a staged migration plan and rollback checklist.

By Ehsan Azish · 3NSOFTS·September 2026·7 min read

A legacy iOS app needs modernization when its implementation prevents the team from shipping reliable changes. Its age, use of UIKit, or remaining Objective-C files do not establish that need on their own.

Refactor when the product works but changes are risky. Introduce SwiftUI when a bounded interface change benefits from it. Consider a rewrite when a required capability cannot reasonably be delivered through the existing architecture—and only after proving the replacement can preserve customer data and essential behavior.

These choices can coexist. A team might retain a reliable Core Data store, replace one networking dependency, and build a new settings flow in SwiftUI. The unit of modernization should be a customer workflow or a technical boundary, not the entire repository by default.

Start with the failure that costs you money

Before choosing a framework, write down the recurring problem and how you will know it improved. For example:

  • Checkout changes repeatedly break restored purchases: measure purchase and restore scenarios across supported versions.
  • A reporting screen becomes unresponsive with large accounts: capture a representative dataset and measure interaction latency.
  • Every release requires manual project repair: prove a clean checkout can produce a signed archive through the documented release process.
  • A discontinued dependency blocks an OS update: identify its callers and replace the smallest stable interface around it.

“Move to SwiftUI” is an implementation proposal. “Allow the team to change the order detail screen without destabilizing inventory” is an outcome. If the proposal does not address the outcome, a new framework can reproduce the same problem with different syntax.

The iOS code review checklist is a useful starting point for collecting evidence before estimating work.

A decision matrix for the first investment

EvidenceStarting approachProof to require
Stable behavior, tangled implementationRefactor behind existing interfacesThe same customer scenarios pass
One screen needs substantial new UIEmbed a SwiftUI featureNavigation, state, and accessibility remain correct
One obsolete dependency blocks releasesReplace that dependency through an adapterNo remaining callers depend on its behavior
Required workflows conflict with core assumptionsPrototype a replacement vertical sliceMigration and cutover work with real data

A rewrite becomes a serious option when several critical boundaries are inseparable and repairing them would approach the cost of replacement. That is a conclusion from investigation, not a percentage of “old code.” Compare both plans using the same definition of done: migrated data, restored purchases, supported devices, operational tooling, and release ownership.

Add SwiftUI without moving the whole app

Apple supports embedding SwiftUI through UIHostingController and using UIHostingConfiguration in UIKit cells. These are practical integration points for staged adoption. See Apple's UIKit integration documentation and Use SwiftUI with UIKit.

Choose a feature with explicit inputs and outputs. A shipment detail screen might receive a shipment identifier, read through the existing repository, and report a navigation action back to its UIKit coordinator. It should not create a second database stack just because its view is written in SwiftUI.

Keep one owner for navigation and one owner for each piece of mutable business state. If both a view model and a legacy controller independently save the same object, the framework boundary becomes a new race condition. Translate values at the boundary and keep write operations in one place.

Use the SwiftUI–UIKit production comparison for framework trade-offs. The migration question here is narrower: where can SwiftUI improve delivery without changing unrelated behavior?

Work through one vertical slice

Consider a hypothetical inventory app with a large UIKit order controller. Editing customer details, calculating prices, and saving orders all happen in that controller. Rebuilding the screen immediately would move poorly defined business rules into new files.

A safer sequence is:

  1. Record how an existing order behaves: discounts, tax rounding, empty fields, interrupted saves, and reopened drafts.
  2. Move price calculation into a deterministic component and run the recorded cases against it.
  3. Put persistence behind an operation such as “save order draft,” retaining the existing store.
  4. Introduce the new interface using those operations.
  5. Release to a controlled audience and compare failures against the old flow.
  6. Remove the old screen after the replacement meets the acceptance criteria.

The result of the first stage is valuable even if the UI migration pauses. That makes the plan easier to stop, revise, or fund in parts.

Separate UI migration from data migration

Changing views and changing storage in the same release makes regressions harder to locate. When possible, preserve the storage contract while replacing presentation. Plan schema changes separately with representative copies of older stores.

Do not assume two persistence frameworks can safely write to the same SQLite file merely because they share implementation ancestry. Apple specifically documents unloading a Core Data store before initializing SwiftData in its CloudKit schema setup workflow to avoid competing sync owners. Treat that as a reason to define ownership carefully, not as permission for arbitrary concurrent access. SwiftData synchronization documentation

For a real storage migration, define which version reads which format, how interruptions recover, and whether the previous binary can still open the new store. Consult the Core Data migration article before changing production schemas.

Make rollback a design decision

A feature flag can restore an old screen. It cannot automatically undo an incompatible database migration or a server-side change. Write a separate recovery plan for each kind of state.

Before release, verify:

  • An upgrade from a supported older app version preserves representative customer records and attachments.
  • Existing purchases and account sessions remain usable.
  • Deep links and notifications still reach the correct feature.
  • VoiceOver, larger text, and keyboard interactions work in the new flow.
  • The previous UI can operate on any data written by the new UI if that is the rollback plan.
  • Someone owns the decision to stop rollout, and the triggering failures are defined.

If those conditions cannot be demonstrated, reduce the first release's scope. A smaller migration with a clear exit is more useful than a broad migration that must succeed all at once.

Estimate the whole transition

Ask for separate estimates for discovery, boundary extraction, replacement, data compatibility, validation, and removal of the old path. A proposal that prices only new screens omits much of the work that makes modernization safe.

The strongest first deliverable is a small release with evidence: a reproducible baseline, one replaced boundary, a passing compatibility checklist, and a remaining-work estimate updated from what the team learned. If you need that evidence before committing to a rewrite, a scoped iOS architecture audit can establish the starting point.

Authoritative References