Skip to main content

SwiftUI or UIKit in 2026: How to Choose SwiftUI vs UIKit

SwiftUI or UIKit in 2026: default to SwiftUI from iOS 17 and wrap UIKit where needed; lead with UIKit for iOS 15 or big UIKit codebases.

SwiftUI or UIKit in 2026: for a new project with a minimum of iOS 17 or later, build the UI in SwiftUI by default and wrap the occasional UIKit control SwiftUI doesn’t cover in UIViewRepresentable. If you need to support iOS 15 or earlier, your team already has a lot of UIKit code, or your core feature is a huge list, rich text editing or heavily customized interaction, make UIKit the base and bring in SwiftUI for new screens through UIHostingController. The two can live side by side in one app, so the real question isn’t which one to use but which one is the foundation.

Below you’ll find recommendations by scenario, the capability differences between the two, the key SwiftUI features available at each minimum iOS version, and code for mixing them. Minimum API versions come from Apple’s developer documentation and the availability annotations in the SDK, as of October 2026.

Should a new project use SwiftUI or UIKit in each scenario?

Scenario Recommendation Why
New app, minimum iOS 17 or later SwiftUI as the base @Observable, SwiftData and NavigationStack all work directly, and state management and navigation are simplest
New app that must support iOS 15 or 16 SwiftUI as the base, written the older way iOS 15 has no NavigationStack, and iOS 16 and earlier have no @Observable or SwiftData, so use NavigationView, ObservableObject, and Core Data or file storage
Must support versions earlier than iOS 15 UIKit as the base Early SwiftUI lacks too much, and many screens would need two versions behind if #available
Lists with tens of thousands of rows, custom layouts, fine control over reuse and prefetching UIKit’s UICollectionView, or a UIKit list inside a SwiftUI shell List and LazyVStack handle ordinary lists, but they don’t give you the control over reuse, prefetching and scroll position that UICollectionView does
Rich text, code editors, chat input fields UIKit’s UITextView, embedded in SwiftUI with UIViewRepresentable TextEditor only supports binding an AttributedString for rich text from iOS 26, and UITextView still offers more control over the cursor, selection and input methods
Heavily customized gestures, transitions, animations tied to scrolling UIKit, or UIKit used locally inside a SwiftUI screen UIViewPropertyAnimator, custom transitions and Core Animation give you frame-level control
Team already has a lot of UIKit code Keep UIKit as the base, write new screens in SwiftUI Bring them in one screen at a time through UIHostingController, with no rewrite
Native parts of Flutter, React Native, uni-app and similar frameworks Integrate through the framework’s interfaces with UIKit, and put SwiftUI inside if you like The native plugin and component interfaces of these frameworks hand you a UIView or UIViewController, so wrap a SwiftUI screen in a UIHostingController first
Home Screen widgets, Live Activities SwiftUI only WidgetKit views are SwiftUI views

For a full widget walkthrough, see iOS widget tutorial with WidgetKit and SwiftUI, built on Windows.

To decide the minimum version, look at your own app’s user distribution first. Without historical data, use Apple’s published numbers. Counting devices that made App Store transactions on June 7, 2026, 79% of iPhones were on iOS 26, and 86% of iPhones introduced in the last four years were. See App Store usage.

How do SwiftUI and UIKit differ?

Area SwiftUI UIKit
Style Declarative. The UI is a function of state, and the system refreshes it when state changes Imperative. You update views yourself when data changes. From iOS 26, @Observable properties read in updateProperties() and layoutSubviews() are tracked automatically. On iOS 18 you turn this on with UIObservationTrackingEnabled in Info.plist
Available features Grow every year with the OS, so a higher minimum version gives you more Cover every current iOS version. The API is stable, and new releases mostly add to it
Lists List, LazyVStack. An editable list takes a few lines UICollectionView with compositional layout and diffable data sources. More code, full control over reuse and prefetching
Navigation NavigationStack (iOS 16), data-driven, with a path you can save and restore UINavigationController with manual push and pop, and custom transitions
Text editing TextField, TextEditor. Rich text binding needs iOS 26 UITextField, UITextView. TextKit 2 is available from iOS 16
Custom layout Layout protocol (iOS 16), Grid (iOS 16) Auto Layout, UICollectionViewLayout subclasses
Animation Implicit animations, matchedGeometryEffect, PhaseAnimator (iOS 17) UIView.animate, UIViewPropertyAnimator, Core Animation. Interactive and interruptible
Data layer SwiftData’s @Query drives views directly Core Data with NSFetchedResultsController. SwiftData works too, but you fetch yourself
Sharing across platforms Much code shared across iOS, iPadOS, macOS, watchOS and visionOS iOS, iPadOS, tvOS, visionOS. macOS needs Mac Catalyst, watchOS isn’t supported
Debugging UI problems The system generates the view tree. Use Self._printChanges() to see which state triggered a refresh View objects map one to one to code, so it’s obvious which view a breakpoint stops in

Each side has things it does more smoothly. SwiftUI removes the boilerplate of keeping state and UI in sync, and UIKit gives you direct control over every view object.

What does SwiftUI offer at each minimum iOS version?

Your deployment target decides whether you can use these APIs directly. Below the version in the table, the compiler reports errors like is only available in iOS 17.0 or newer, and you have to wrap the code in if #available and write a second version.

Minimum iOS Key features available from this version What it means for your choice
15 AsyncImage, .task, .refreshable, .searchable, swipeActions Async loading, pull to refresh, search and swipe actions are all there, so ordinary screens can be pure SwiftUI
16 NavigationStack, NavigationSplitView, Swift Charts, Grid, the Layout protocol, ViewThatFits, presentationDetents. On the UIKit side, UIHostingConfiguration lets you write list cells in SwiftUI Navigation and custom layout are complete. This is the practical starting point for SwiftUI as the base
17 @Observable, @Bindable, SwiftData, ContentUnavailableView, scrollTargetBehavior, scrollPosition(id:), inspector, sensoryFeedback, the new onChange State management and the local database move to the new style, with noticeably less boilerplate
18 The new Tab syntax, MeshGradient, ScrollPosition, onScrollGeometryChange, SwiftData’s #Unique and #Index Reading, setting and observing scroll position is more complete
26 SwiftUI’s WebView, TextEditor bound to AttributedString, glassEffect. On the UIKit side, updateProperties() and automatic observation tracking on by default Rich text editing and web content no longer need UIKit wrappers

For more detailed APIs and fallbacks for older versions, see the version table in Write SwiftUI with AI: prompt templates and common pitfalls. For SwiftData in depth, see Persist data locally with SwiftData: models, queries, migration and device testing.

How do you mix SwiftUI and UIKit?

The code below compiles with a deployment target of iOS 15 in both the Swift 5 and Swift 6 language modes.

Push a SwiftUI screen from UIKit

UIHostingController wraps a SwiftUI view in an ordinary UIViewController that you can push, present or put in a UITabBarController. Replace delegate callbacks with closure parameters.

 1import SwiftUI
 2import UIKit
 3
 4struct ProfileView: View {
 5    let name: String
 6    var onLogout: () -> Void
 7
 8    var body: some View {
 9        List {
10            Text(name)
11            Button("Log Out", role: .destructive, action: onLogout)
12        }
13        .navigationTitle("Profile")
14    }
15}
16
17final class MainViewController: UIViewController {
18    func showProfile() {
19        let profile = ProfileView(name: "Alex") { [weak self] in
20            self?.logout()
21        }
22        let host = UIHostingController(rootView: profile)
23        navigationController?.pushViewController(host, animated: true)
24    }
25
26    private func logout() {
27        navigationController?.popToRootViewController(animated: true)
28    }
29}

A pushed SwiftUI screen uses the navigation bar of the outer UINavigationController, and .navigationTitle shows there. Don’t wrap the screen in another NavigationStack. Capture the controller with [weak self] in the closure. For why, see Find and fix Swift retain cycles and memory leaks on a real iPhone.

Embed SwiftUI in part of a UIKit screen

To replace only part of a screen with SwiftUI, add a UIHostingController as a child view controller. You need all three steps, addChild, addSubview and didMove(toParent:). Without addChild the view controller hierarchy is incomplete, and changes like safe areas and appearance don’t reach the SwiftUI content.

 1struct StatsCard: View {
 2    var count: Int
 3    var body: some View {
 4        Text("\(count) done today")
 5            .font(.headline)
 6            .padding()
 7    }
 8}
 9
10final class DashboardViewController: UIViewController {
11    private lazy var statsHost = UIHostingController(rootView: StatsCard(count: 0))
12
13    override func viewDidLoad() {
14        super.viewDidLoad()
15        addChild(statsHost)
16        statsHost.view.translatesAutoresizingMaskIntoConstraints = false
17        view.addSubview(statsHost.view)
18        NSLayoutConstraint.activate([
19            statsHost.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
20            statsHost.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
21            statsHost.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
22        ])
23        statsHost.didMove(toParent: self)
24    }
25
26    func update(count: Int) {
27        statsHost.rootView = StatsCard(count: count)
28    }
29}

When the data changes, assign a new value to rootView. If the embedded SwiftUI content changes size and the outer Auto Layout should follow, set sizingOptions to .intrinsicContentSize on iOS 16 and later.

Use UIKit controls in SwiftUI

UIViewRepresentable wraps a UIView as a SwiftUI view. makeUIView is called once to create the view. updateUIView is called when SwiftUI state changes and pushes the new values in. A Coordinator receives UIKit delegate callbacks and writes them back to the binding.

 1struct RichTextView: UIViewRepresentable {
 2    @Binding var text: String
 3
 4    func makeUIView(context: Context) -> UITextView {
 5        let view = UITextView()
 6        view.font = .preferredFont(forTextStyle: .body)
 7        view.delegate = context.coordinator
 8        return view
 9    }
10
11    func updateUIView(_ view: UITextView, context: Context) {
12        if view.text != text {
13            view.text = text
14        }
15    }
16
17    func makeCoordinator() -> Coordinator {
18        Coordinator(text: $text)
19    }
20
21    final class Coordinator: NSObject, UITextViewDelegate {
22        var text: Binding<String>
23
24        init(text: Binding<String>) {
25            self.text = text
26        }
27
28        func textViewDidChange(_ textView: UITextView) {
29            text.wrappedValue = textView.text
30        }
31    }
32}
33
34struct NoteEditor: View {
35    @State private var note = ""
36
37    var body: some View {
38        RichTextView(text: $note)
39            .frame(minHeight: 200)
40    }
41}

Compare before assigning in updateUIView. Assigning view.text every time moves the cursor to the end and interrupts input methods that compose text, such as Chinese, Japanese and Korean keyboards. To wrap a whole view controller, such as the camera or a document picker, use UIViewControllerRepresentable in the same way.

Write UIKit list cells in SwiftUI

Keep the list itself in UICollectionView and write the cell content in SwiftUI. On iOS 16 and later, use UIHostingConfiguration instead of creating a UIHostingController for each cell.

1@available(iOS 16.0, *)
2final class OrderCell: UICollectionViewCell {
3    func configure(title: String) {
4        contentConfiguration = UIHostingConfiguration {
5            Label(title, systemImage: "shippingbox")
6        }
7    }
8}

This is a common compromise, with UIKit for complex lists and SwiftUI for cell styling. UICollectionView still manages reuse and prefetching.

For the official reference, see Apple’s documentation for UIHostingController, UIViewRepresentable and UIHostingConfiguration. For automatic observation tracking in UIKit, see Updating views automatically with observation tracking in UIKit.

How do you bring SwiftUI into an existing UIKit project gradually?

  • Start with new screens. Screens with simple state, like settings, about and forms, are the best first candidates for SwiftUI. Connect them to your existing navigation with UIHostingController at the entry point.
  • Pull the data layer out first. Move network requests and business logic from view controllers into separate classes that UIKit and SwiftUI screens share. With a minimum of iOS 17, the class can use @Observable and both sides can observe it.
  • Keep one navigation system. When UIKit is the base, let UINavigationController handle navigation and don’t wrap SwiftUI screens in NavigationStack, or you get two navigation bars.
  • Don’t rush to rewrite old screens. Rewriting complex screens that run reliably has little payoff and real risk. Replace them when a major feature change comes along.

Create either kind of project in KXBuild

When you create a project with KXBuild on Windows, Linux or macOS, the templates include iOS App (SwiftUI) and iOS App (Swift, UIKit), with the command-line ids app_swiftui and app_swift. Both target iOS 15. After settling SwiftUI or UIKit with the recommendations above, change iOS Deployment Target on the General page of KXBuild: Edit Target Settings. See Built-in templates and Project settings.

FAQ

Can SwiftUI fully replace UIKit now?

Most ordinary screens can be built in SwiftUI alone, but UIKit is still underneath, and some features need UIKit controls through UIViewRepresentable or UIViewControllerRepresentable. For fine control over huge lists, complex text editing and custom transitions, UIKit is still more direct. Mixing both in one app is normal.

Should I learn SwiftUI or UIKit first for iOS development in 2026?

Learn SwiftUI first. It gets you to a running app fastest, and most new projects use it as the base. Then pick up UIKit’s view controller lifecycle, navigation and UITableView or UICollectionView. You’ll need them for taking over older projects, writing native plugins for cross-platform frameworks and wrapping controls SwiftUI doesn’t have.

Is SwiftUI slower than UIKit?

On ordinary screens users can’t tell the difference. Stutters usually come from slow work in body, state scoped so broadly that the whole screen refreshes, or unstable ids in ForEach. For lists with lots of data or fine control over reuse while scrolling, UICollectionView is easier to tune. Measure the bottleneck on a device before deciding, rather than going by reputation.

Should I rewrite an old UIKit project entirely in SwiftUI?

No. Write new screens in SwiftUI and connect them to your existing navigation through UIHostingController, and replace old screens when their features change substantially. A full rewrite means retesting everything and gives users nothing directly.

What minimum iOS version should I support?

Look at your own app’s OS distribution first, then at Apple’s published data. As of June 7, 2026, 79% of iPhones were on iOS 26. With iOS 17 you can use @Observable and SwiftData, with iOS 16 you can use NavigationStack, and every version you go lower means giving up a set of APIs or writing extra compatibility code.