When to Choose SwiftUI Over UIKit
Since its introduction in iOS 13, SwiftUI has fundamentally changed how developers build user interfaces for Apple platforms. However, UIKit remains a mature, battle-tested framework that powers millions of existing apps. Deciding between the two is rarely a binary choice — it depends on your project requirements, team expertise, deployment target, and long-term maintenance goals. This tutorial walks you through the key considerations, practical trade-offs, and concrete examples to help you make an informed decision.
What Is SwiftUI?
SwiftUI is Apple's declarative UI framework that lets you describe your interface using simple Swift code. Instead of imperatively configuring views and updating them when state changes, you declare what the UI should look like for a given state, and SwiftUI handles the rendering and updates automatically.
UIKit, by contrast, is an imperative, object-oriented framework built around UIView and UIViewController. You create views, add them to a hierarchy, configure their properties, and manually respond to lifecycle and layout events.
Why the Choice Matters
Choosing the right framework affects more than just syntax. It influences development speed, onboarding time, code maintainability, and your ability to leverage new Apple features. SwiftUI can dramatically reduce boilerplate, but it also introduces constraints around customization, backward compatibility, and access to lower-level APIs. Understanding these trade-offs early prevents costly rewrites later.
When SwiftUI Is the Right Choice
New Projects Targeting iOS 15+
If you are starting a new app and your minimum deployment target is iOS 15 or later, SwiftUI is generally the best starting point. By iOS 15, most of the early rough edges had been smoothed out, and critical APIs like List, Form, navigation, and async life cycle support had matured significantly.
import SwiftUI
struct ContentView: View {
@State private var items: [String] = ["Apple", "Banana", "Cherry"]
var body: some View {
NavigationStack {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
.navigationTitle("Fruits")
.toolbar {
EditButton()
}
}
}
}
The same screen in UIKit would require a UITableViewController, data source methods, editing delegate callbacks, and manual navigation controller setup — easily three to four times the code.
Rapid Prototyping and MVPs
SwiftUI excels when speed matters. Live previews in Xcode let you iterate on designs without running the full app. This makes it ideal for prototypes, internal tools, and minimum viable products where visual feedback loops are critical.
#Preview {
ContentView()
.preferredColorScheme(.dark)
}
Cross-Platform Apple Ecosystem Apps
If your app needs to run on iOS, iPadOS, macOS, watchOS, and tvOS, SwiftUI is the clear winner. A significant portion of your view code can be shared across platforms, something that is nearly impossible with UIKit (which does not exist on watchOS or tvOS in the same form).
Forms and Settings Screens
SwiftUI's Form container makes building settings and data-entry screens trivial. What used to require static table view cells and custom cells in UIKit is now a few lines of declarative code.
struct SettingsView: View {
@AppStorage("notificationsEnabled") private var notificationsEnabled = true
@AppStorage("theme") private var theme = "Light"
var body: some View {
Form {
Section("Preferences") {
Toggle("Enable Notifications", isOn: $notificationsEnabled)
Picker("Theme", selection: $theme) {
Text("Light").tag("Light")
Text("Dark").tag("Dark")
Text("System").tag("System")
}
}
}
}
}
When UIKit Is Still the Better Choice
Supporting iOS 13 or Earlier
SwiftUI on iOS 13 is missing many essential features, including List reordering improvements, LazyVStack, AsyncImage, and reliable navigation APIs. If your deployment target must include iOS 13, UIKit is often more practical for complex interfaces.
Highly Customized, Pixel-Perfect Layouts
SwiftUI's layout system is powerful but opinionated. For apps requiring fine-grained control over text layout, custom collection view behaviors, or complex gesture recognizers that interact with scroll views, UIKit still offers more predictable control.
Large Existing UIKit Codebases
If you have a mature UIKit app, a full rewrite is rarely justified. Instead, adopt SwiftUI incrementally using UIHostingController to embed SwiftUI views, or UIViewRepresentable to wrap existing UIKit components.
import SwiftUI
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
let hostingController = UIHostingController(rootView: ProfileView())
window.rootViewController = hostingController
window.makeKeyAndVisible()
self.window = window
}
}
struct ProfileView: View {
var body: some View {
Text("Embedded SwiftUI in a UIKit window")
.padding()
}
}
Advanced Text and Custom Drawing
While SwiftUI has Canvas and TextEditor, apps with rich text editing, complex Core Text layouts, or custom PDF generation often still rely on UITextView, TextKit, and Core Graphics directly through UIKit.
How to Mix SwiftUI and UIKit
Wrapping UIKit Views in SwiftUI
Use UIViewRepresentable to bring UIKit components into SwiftUI. This is useful when you need a specific control that SwiftUI does not yet provide, such as MKMapView or a custom camera preview.
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
@Binding var region: MKCoordinateRegion
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
view.setRegion(region, animated: true)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
let parent: MapView
init(_ parent: MapView) { self.parent = parent }
}
}
Embedding SwiftUI in UIKit
Conversely, UIHostingController lets you present SwiftUI views from UIKit view controllers, enabling a gradual migration path.
let swiftUIView = FeatureView(viewModel: FeatureViewModel())
let hostingController = UIHostingController(rootView: swiftUIView)
navigationController?.pushViewController(hostingController, animated: true)
Best Practices
- Set a sensible minimum deployment target. SwiftUI shines on iOS 15+. If you must support older versions, consider hybrid approaches or sticking with UIKit.
- Keep view bodies small and focused. Break large views into smaller subviews. SwiftUI re-evaluates
bodywhen state changes, so lean views improve performance. - Use
@StateObjectfor owned view models. Avoid@Statefor reference types.@StateObjectensures the object is created once and persists across view updates. - Prefer value types for model data. SwiftUI is optimized for value semantics. Structs work naturally with state bindings and diffing.
- Don't fight the framework. If you find yourself writing complex workarounds for layout or navigation, evaluate whether UIKit would be simpler for that screen.
- Measure before migrating. Use Instruments to compare performance. SwiftUI is fast for most cases, but heavy lists with complex cells may still benefit from UIKit's
UICollectionViewprefetching. - Adopt SwiftUI incrementally in legacy apps. Start with leaf screens like settings or onboarding, then expand as your team gains confidence.
Conclusion
SwiftUI and UIKit are not adversaries — they are complementary tools in the Apple developer ecosystem. For new projects targeting modern iOS versions, SwiftUI offers faster development, cleaner code, and cross-platform potential that UIKit cannot match. For apps with legacy constraints, highly customized interfaces, or complex text and drawing needs, UIKit remains a robust and reliable choice. The most pragmatic approach is often hybrid: use SwiftUI where its declarative power shines, and reach for UIKit when you need precise control. By understanding the strengths and limitations of each framework, you can make architectural decisions that keep your codebase maintainable and your development velocity high for years to come.