DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI App Migration Strategies (UIKit SwiftUI, Legacy Modern)

Most real apps don’t start in SwiftUI.

They start as:

  • UIKit apps
  • storyboards
  • legacy MVVM or MVC
  • years of technical debt
  • business-critical code you can’t break

“Rewrite everything in SwiftUI” is not a strategy.

It’s how apps die.

This post shows how to safely migrate real apps to SwiftUI, incrementally, without:

  • freezing development
  • breaking production
  • losing sanity
  • rewriting everything twice

🧠 The Core Principle

Migration is about containment, not conversion.

You don’t replace an app.

You replace edges, one feature at a time.


🧱 1. Never Start at the Root

❌ Bad idea:

  • rewriting AppDelegate
  • replacing navigation
  • rebuilding the entire app shell

✅ Correct approach:

  • migrate leaf features
  • isolated screens
  • low-risk flows

Think:

“What can I remove later without touching anything else?”


🧩 2. SwiftUI Inside UIKit (First Phase)

This is the safest entry point.

let vc = UIHostingController(
    rootView: ProfileView(viewModel: vm)
)
navigationController.pushViewController(vc, animated: true)
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • zero navigation rewrite
  • zero app lifecycle changes
  • feature-by-feature migration
  • rollback is trivial

UIKit remains the host. SwiftUI is a guest.


🧠 3. Feature Boundary Is Everything

Before migrating, define a boundary:

Profile Feature
- View
- ViewModel
- Repository
- State
Enter fullscreen mode Exit fullscreen mode

UIKit owns:

  • navigation
  • lifecycle

SwiftUI owns:

  • rendering
  • local state
  • interactions

No cross-contamination.


🧭 4. Shared ViewModels Between UIKit & SwiftUI

Your ViewModels should not know about UI frameworks.

final class ProfileViewModel: ObservableObject {
    @Published var user: User
}
Enter fullscreen mode Exit fullscreen mode

UIKit:

viewModel.$user
    .sink { ... }
Enter fullscreen mode Exit fullscreen mode

SwiftUI:

@ObservedObject var viewModel: ProfileViewModel
Enter fullscreen mode Exit fullscreen mode

Same logic. Two UIs.


🧱 5. Gradually Move Navigation to SwiftUI

Once enough features are migrated:

  1. SwiftUI screens push SwiftUI screens
  2. UIKit only hosts entry points
  3. Eventually replace root navigation

Never migrate navigation first.


🧬 6. SwiftUI → UIKit (Reverse Hosting)

Some legacy components should stay UIKit:

  • camera
  • media pickers
  • deeply custom views

Use:

struct LegacyView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        LegacyViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
Enter fullscreen mode Exit fullscreen mode

SwiftUI becomes the host when ready.


🧠 7. State Migration Strategy

Do not move global state early.

Bad:

  • rewriting AppState
  • moving session logic

Good:

  • migrate local feature state
  • keep global state shared
  • refactor AppState later when SwiftUI dominates

🧪 8. Testing During Migration

Golden rule:

Every migrated feature must be easier to test than before.

SwiftUI gives you:

  • preview-based validation
  • ViewModel testing
  • snapshot testing

If a migration makes testing worse, stop.


⚠️ 9. Avoid the “Hybrid Hell” Trap

Hybrid apps fail when:

  • UIKit calls SwiftUI logic
  • SwiftUI calls UIKit logic
  • shared state is mutated everywhere
  • ownership is unclear

Rule:

  • UIKit hosts
  • SwiftUI renders
  • ViewModels coordinate

Never mix responsibilities.


❌ 10. Common Migration Anti-Patterns

Avoid:

  • rewriting everything at once
  • migrating core infrastructure first
  • mixing UIKit & SwiftUI inside the same screen
  • copying UIKit patterns into SwiftUI
  • forcing SwiftUI to behave like UIKit
  • blocking releases during migration

Migration must be invisible to users.


🧠 Mental Model

Think:

Legacy App
 → SwiftUI Feature
   → SwiftUI Feature
     → SwiftUI Navigation
       → SwiftUI App
Enter fullscreen mode Exit fullscreen mode

Each step must be:

  • reversible
  • incremental
  • shippable

🚀 Final Thoughts

Successful SwiftUI migration:

  • is gradual
  • respects existing architecture
  • improves quality incrementally
  • never blocks shipping
  • never risks the business

SwiftUI is not a destination.
It’s a tool for modernizing safely.

Top comments (0)