After 11 years building iOS apps, here's how I actually think about choosing between SwiftUI and UIKit — and why the question itself is often wrong.
When SwiftUI shipped in 2019, the hot take was inevitable: UIKit is dead.
Five years on, I’m still writing UIKit code every week. So is every senior iOS engineer I know.
That doesn’t mean SwiftUI lost. It means the original question was wrong.
Most SwiftUI vs UIKit debates start from the wrong place. They treat it like a football rivalry — pick a side, defend it, dismiss the other.
The question I actually ask before starting any new feature is simpler:
What does this screen need to do, and what’s the cheapest way to make it maintainable five years from now?
That question leads somewhere more useful than “UIKit is legacy” or “SwiftUI is the future.”
UIKit is imperative. You tell it how to update the UI — move this view, set this label, constrain that to the bottom of the safe area.
That verbosity gets mocked a lot. But here’s what that verbosity buys you: a crystal-clear execution model. You always know what runs, when it runs, and in what order.
At END. Clothing, when we profiled performance issues in high-traffic raffle launches, the problem was always findable. We could trace layout passes, pinpoint reuse cell issues, measure main thread blocking with Instruments — and fix exactly what was wrong.
UIKit doesn’t abstract the rendering pipeline away from you. That’s a feature, not a flaw, when performance actually matters.
SwiftUI is declarative. You describe what the UI should look like given a particular state — and the framework figures out how to get there.
struct ContentView: View {
@State private var isLoading = false
var body: some View {
if isLoading {
ProgressView()
} else {
ContentList()
}
}
}
This is genuinely powerful. You stop thinking about transitions between states and start thinking about what each state looks like. For most UI work — forms, standard lists, settings screens, onboarding flows — this mental model is faster and produces cleaner code.
The trade-off is that SwiftUI’s rendering pipeline is largely opaque. When performance degrades in deeply nested hierarchies or complex list layouts, diagnosing it requires understanding SwiftUI’s identity and diffing system, which is non-trivial and still partially undocumented.
I won’t pretend UIKit is always the right call. But there are specific contexts where reaching for UIKit is the professional choice, not the lazy one.
Pixel-perfect custom layouts. UICollectionViewLayout gives you complete control over how cells are positioned, sized, and animated. You can override layout attributes, build custom invalidation contexts, control scroll snapping precisely. SwiftUI’s LazyVGrid and LazyHGrid are great — until they’re not, and then you’re bridging back to UIKit anyway.
High-frequency updates. A feed that updates dozens of times per second, a trading dashboard, a live activity with tight constraints — UIKit’s explicit control over render cycles is an advantage here. SwiftUI’s state invalidation can cause over-rendering if you’re not careful about how you model state.
Large design systems. If you’re building a shared component library that needs to work predictably across a team of twenty engineers on a five-year-old codebase, UIKit’s stability and explicit API contracts are worth the setup cost.
Legacy codebases. Rewriting working software is almost never the right call. Wrapping UIKit screens in UIViewControllerRepresentable and extending SwiftUI inward is a more pragmatic approach than a full migration.
Here’s what the actual architecture looks like at mature iOS shops:
UIViewRepresentable where needed// Bringing UIKit into SwiftUI when you need the control
struct VideoPlayerWrapper: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = AVPlayer(url: url)
return controller
}
func updateUIViewController(_ controller: AVPlayerViewController, context: Context) {}
}
This isn’t compromise. This is pragmatism. The best iOS codebases I’ve worked in don’t have a framework ideology — they have engineers who use the right tool for the specific problem in front of them.
If you’re a senior engineer, the question isn’t which framework you prefer. It’s whether your framework preference is accidentally becoming a technical constraint.
If you only know UIKit deeply, you’re slowing your team down on new features that would be trivially fast in SwiftUI. If you only know SwiftUI, you’ll eventually hit a wall in any production app that’s under real performance or customisation pressure.
The engineers I’ve learned the most from move between both without drama. They don’t have a strong opinion about which is “better.” They have strong opinions about what a specific screen needs — and they reach for the framework that delivers it cleanest.
UIKit gives you power. SwiftUI gives you speed. Knowing when you need which one — that’s the actual skill.
I’ve been building iOS apps since 2014. These are my genuine views, shaped by real production apps — not benchmarks or toy examples.