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)
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
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
}
UIKit:
viewModel.$user
.sink { ... }
SwiftUI:
@ObservedObject var viewModel: ProfileViewModel
Same logic. Two UIs.
🧱 5. Gradually Move Navigation to SwiftUI
Once enough features are migrated:
- SwiftUI screens push SwiftUI screens
- UIKit only hosts entry points
- 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) {}
}
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
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)