Introduction
I had a feeling that most of the devs that work with Apple Platforms (iOS, macOS, tvOS, etc) prefers SwiftUI over UIKit. I didn't found any data that proved this feeling, so I created a pool in LinkedIn, and that was the result:
This is not the most academic way to gather data, but it reveals that my feeling maybe is right: most of the devs prefers SwiftUI. Some of them, specially the most experienced ones, can argue about some pros and cons of SwiftUI and even scenarios where it should be avoided (mostly mention old OS support and team knowledge about the framework). However, I see a lot of engineers struggle to explain why they prefer SwiftUI. Sometimes it seems that they were told that SwiftUI is better, and they just accepted that. Today we're going to explore the real and impactful differences between SwiftUI and UIKit.
What SwiftUI is not
To compare SwiftUI with anything else we first need to understand what it is. Actually, for this specific UI framework I think it's even more relevant understand what SwiftUI is not. That's because I've heard a lot of wrong explanations and misconceptions about the tool in my career.
🚨 Attention: SwiftUI is not an UIKit wrapper, neither built on top of UIKit.
However, it's possible that SwiftUI uses some UIKit components behind the scenes. More on that below.
So What Is It?
SwiftUI is an UI framework developed by Apple and announced in 20191, introduced in iOS 13 and macOS 10.15 (Catalina). SwiftUI comes to be an alternative to the existing ways of writing UI in these platforms (UIKit and AppKit, respectively). The idea was having a declarative way of writing the UI logic.
The focus of the framework on its declarative syntax led a lot of people to think that SwiftUI was an UIKit library, that converts the SwiftUI declarative code into the UIKit one, which is a totally incorrect concept.
SwiftUI has a total unique way of building UI. The main difference compared to the older framework is that SwiftUI decides how to render a view in runtime. Based in several variables (that are not visible to the developer), such as view complexity, hardware resources availability and more, SwiftUI decides what is the best strategy to render an specific view: either using UIKit/AppKit (if available for that platform) or using Metal to draw the component completely from scratch.
Another good example that illustrates that SwiftUI cannot be an UIKit wrapper is case of visionOS (Apple Vision Pro operating system), where SwiftUI is the only UI framework available for UI development. There's not UIKit, AppKit, WatchKit, or any other previous UI frameworks in this OS. It means that, in this platform, the SwiftUI runtime will always decide to create the UI components from zero.
Commonly Mentioned Advantages of SwiftUI (That May Not Be True)
As I mentioned in the Introduction, most of the engineers understand that SwiftUI is better than UIKit, but sometimes they struggle to explain why. Below I'll cover some of the usual mentioned advantages and why they're sometimes incorrect or incomplete.
Declarative Syntax is Better
The Xcode Interface Builder (the tool to manipulate storyboards and .xib files) has several flaws: bad performance, merging difficulty, code review difficulty, and more. So SwiftUI clearly have an advantage over it, where you can write the code for the UI. However, you can also to the same in UIKit, with ViewCode. If you want to learn more about that, I have a post about how to use ViewCode with UIKit.
That said, what is commonly said as a SwiftUI benefit over UIKit is the declarative syntax: instead of "saying" to the UI do something (UIKit's imperative syntax), you say how it should look/behave. That is mostly true, but let's analyze two equivalent implementations in both frameworks:
UIKit:
let label = UILabel()
label.text = "Hello, World!" // Set the text to "Hello, World!" now. (Imperative)
label.textColor = .red // Set the text color to red now. (Imperative)
SwiftUI:
Text("Hello, World!") // You should've a text "Hello, World!". (Declarative)
.foregroundColor(.red) // The color of the text should be red. (Declarative)
Can you really say that the second one is easier to be written? At least, easier enough for worthing using a totally new technology in your app?
It's true that in UIKit you "order" the framework to do things, in an imperative way, while in SwiftUI you tell it how it should behave when it is rendered, but why the second option is better? Just saying "SwiftUI is better because it's declarative" means nothing if you don't understand what being declarative really changes. Also, some imperative codes can have a similar complexity than declarative ones. We'll see in a bit when it really matters.
Xcode Preview
The Xcode Preview is a very cool tool (when it works) that shows you how your view looks like while you are creating it, "without" needing to build and launch the app to see the changes (actually Xcode performs a basic build process to display the preview, but it's very much faster than a regular app build).
I'll not explore the fact that Xcode Preview is very unstable, you probably already experienced that, but there's something that I'd like to mention that a lot of people don't know: Xcode Preview supports UIKit and AppKit, natively!
Yes, that's right, no workarounds, Xcode just supports it. You can check it here in Apple Developer Documentation. Since Xcode 15, you can use the #Preview
macro to return an UIView
or even an UIViewController
object that the preview will display it.
So if you're choosing SwiftUI because "it has previews", maybe you're choosing that for the wrong reasons.
SwiftUI Is More Future-Proof
SwiftUI is already the official Apple way of writing UI for apps in all platforms. I feel that what we are experiencing with SwiftUI and UIKit nowadays is similar (but not equal) to the transition between Objective-C and Swift. Objective-C is still supported today, but it's considered a legacy technology.
However, while SwiftUI is becoming more robust and stable at each version, UIKit keeps pretty reliable, receiving updates and improvements, as the ones in the last WWDC. Starting a new app with UIKit nowadays can be considered a good idea, depending on some variables. But specially learning it, I'd say that is essential.
Reactivity
Another common advantage of SwiftUI over UIKit mentioned is Reactivity. In SwiftUI you can write Reactive Code, using States
and Observed/Observable Objects
. That's a totally new paradigm that is not natively present in UIKit, where you needed to imperatively set the state to the view, instead of binding a view to a state.
UIKit:
var myText = "Hello, World!"
label.text = myText
SwiftUI:
@State
var myText = "Hello, World"
Text(myText)
1. Binding vs Setting
Again, simply saying that in SwiftUI you bind state to the view instead of imperatively setting it, isn't enough to convince that it's better. It's important to know what data binding really brings to development process.
2. You can write Reactive code in UIKit
In UIKit, it's possible to write code that "reacts" to the state change:
var myText = "Hello, World!" {
didSet {
label.text = myText
}
}
The implementation above is simple and a little limited, but it's also possible adding reactiveness to your code using more robust design patterns, as Observer
and Notification
. Also, you can use some libraries, such as Combine
, or yet the combination of RxSwift
with RxCocoa
to implement Reactive Code. The later allows you to bind state changes to the UI components, similar to what you do in SwiftUI.
What Being Declarative Really Means to SwiftUI
We just saw that simply saying that in declarative code you tell the OS how the view should look instead of asking it to do things isn't enough to convince that it's better.
For me, one of the main differences between the declarative syntax of SwiftUI and the UIKit's imperative one is the responsibility of holding the views instances. Let's compare two different similar codes, one in each framework
UIKit:
final class HomeViewController: UIViewController {
// I need to store the UILabel instance in a property somewhere, a concern that I don't have in SwiftUI
lazy var label: UILabel = {
let label = UILabel()
label.text = "Hello, World!"
return label
}()
override func viewDidLoad() {
// The "effort" of adding the label to view hierarchy here is similar to SwiftUI
view.addSubview(label)
}
}
SwiftUI:
struct HomeView: View {
var body: some View {
// Doing this is "so hard as" adding a subview in UIKit, but here I don't need to worry about holding a reference of `Text`
Text("Hello, World!")
}
}
In SwiftUI, the framework completely handles the components lifecycle, being able to reuse or simply discard it without any interaction of the developer. In UIKit on the other hand, the developer is responsible for creating, holding and sometimes deallocating these objects manually.
Also, in UIKit they are responsible for programming when all these things happens. For example: in the piece of code above, the UILabel
object was created in a lazy variable, which is a good strategy to improve memory usage. In SwiftUI the developer simply doesn't need to think about it. In the newer framework, the views are value types, which eliminates the concern about allocation/deallocation.
In my opinion, that's something that makes SwiftUI really different from UIKit.
Updating the View
Another great example of a difference that the declarative syntax allows SwiftUI to have is the way that the framework updates the view. In UIKit, the developer explicitly say what should change in the view (as reloading a CollectionView, for example). In SwiftUI, the declarative syntax allows the framework decides which strategy it will use to update the view. We'll see that in details in the Diffing Algorithm section.
Reactivity
As I said, you can have reactive code in UIKit. But the thing with SwiftUI and Reactivity is that in SwiftUI, this concept is already built-in. You don't need to implement any design pattern from scratch, such as Observer, Notification or even using frameworks as RxSwift and Combine.
The UI framework has already a default implementation (@State
and/or @StateObject
), designed to work properly with the UI framework. The Reactivity in SwiftUI eliminates the need of managing subscriptions, lifecycle of observable objects and drastically reduces the effort to avoid data races.
So SwiftUI isn't better than UIKit because it supports reactive programming, it is better because in SwiftUI, reactivity is easier. For example, these two code examples are conceptually equivalent:
UIKit:
import UIKit
import RxSwift
import RxCocoa
class MyCollectionViewCell: UICollectionViewCell {
private let disposeBag = DisposeBag()
private let countRelay = BehaviorRelay<Int>(value: 0)
var countObservable: Observable<Int> {
return countRelay.asObservable()
}
private let countLabel: UILabel = {
// ...
}()
private let incrementButton: UIButton = {
// ...
}()
private func incrementCount() {
// Thread-safe modification of the relay
let newValue = countRelay.value + 1
countRelay.accept(newValue)
}
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupBindings()
}
private func setupUI() {
// Add views to hierarchy and add constraints
}
private func setupBindings() {
// Button taps increment the relay (main thread guaranteed by RxCocoa)
incrementButton.rx.tap
.observe(on: MainScheduler.instance) // Ensure main thread
.subscribe(onNext: { [weak self] in
self?.incrementCount()
})
.disposed(by: disposeBag)
// Label reacts to relay changes (main thread guaranteed)
countRelay
.observe(on: MainScheduler.instance)
.map { "\($0)" }
.bind(to: countLabel.rx.text)
.disposed(by: disposeBag)
}
override func prepareForReuse() {
super.prepareForReuse()
// Create a new dispose bag to cancel all subscriptions
disposeBag = DisposeBag()
// Reset UI to default state
countLabel.text = nil
}
}
SwiftUI:
import SwiftUI
struct CounterCell: View {
@State private var count = 0
var body: some View {
VStack {
Text("\(count)")
Button("Increment") {
count += 1
}
}
}
}
In both examples we have a label, that displays a integer value stored in views' state, and a button that increments it. In both examples, the label reacts to the state changes, and the button modifies the state. But the amount of code to achieve it is completely different.
Observe that, in the UIKit example, since we are inside an UICollectionViewCell
, we should take care of holding the subscriptions in the cell instance, since the dequeue strategy will reuse the same object. So it's needed to reset the disposeBag
under the prepareForReuse()
. In SwiftUI, since the view is a value type, you don't need to think about object reuse. For the newer framework, it's easier to simply destroy and recreate the view when needed.
ViewModifiers
vs properties of UIView
objects
One of the key differences between SwiftUI and UIKit lies in how we change the appearance and behavior of views. In UIKit, views are mutable — you can directly change the properties of a UIView
instance. For example, setting view.backgroundColor = .red
modifies the same view object in memory. In SwiftUI, on the other hand, views are immutable:
To add styling to views in SwiftUI, we need to use ViewModifiers
. When you apply a ViewModifier
like .padding()
or .background()
, you're not modifying the original view — you're creating a new view that wraps the previous one, with the modifier applied. Each chained modifier returns a new view value, building up a view hierarchy in a declarative way.
Because of this, the order in which you apply modifiers matters. For instance, .background(Color.red).padding()
will produce a different layout than .padding().background(Color.red)
, because each modifier wraps the result of the previous one.
![]() |
![]() |
---|
That happens because, in the first case, the background is applied over the current computed view size (100x100), and then a padding is applied to a view that already have a red background of this size. In the second case, the padding()
modifier creates a new view, that is bigger than the original one, and the background is applied to this new view.
This immutability characteristic enables SwiftUI to use a smart rendering system under the hood — the Diffing Algorithm.
Diffing Algorithm
Because SwiftUI views are value types, they don’t hold identity in the traditional sense. Instead, SwiftUI needs to compare the old and new view hierarchies to determine what actually changed between frames. This process is handled by its internal diffing algorithm.
The goal of the diffing algorithm is to compute the minimal number of changes needed to bring the current view tree in sync with the new one — avoiding unnecessary work, re-renders, or animations.
Let’s look at a simple example:
// Initial state
VStack {
Text("A") // ID: 1
Text("B") // ID: 2
}
// Updated state
VStack {
Text("C") // ID: 3 (new)
Text("B") // ID: 2 (already exists)
}
How SwiftUI "sees" the view state:
Initial View Hierarchy: Updated View Hierarchy:
VStack VStack
├── Text("A") [ID: 1] ├── Text("C") [ID: 3] ← inserted
└── Text("B") [ID: 2] └── Text("B") [ID: 2] ← reused
In this case, when the state changes SwiftUI will:
- Recreate the entire view state tree (not the rendered view hierarchy yet).
- Compare both trees.
- Recognize that the
Text("B")
with ID2
is still present, so it does nothing to that view. - Identify
Text("A")
(ID1
) is no longer in the hierarchy and remove it from the view. - Add
Text("C")
(ID3
) view at the beginning.
💡 Tip: SwiftUI automatically creates IDs for views. However, when views are inside a
ForEach
, you need to manually assign an.id()
to each item. Assigning a unique and stable value helps SwiftUI perform a smarter diff. Without proper IDs, SwiftUI may destroy and recreate views unnecessarily, or yet not recreate them when necessary.
This behavior is completely different compared to UIKit, where you manually update existing views or remove/add them yourself. SwiftUI’s declarative model and diffing engine let you focus on what the UI should look like, and the framework handles how to get there behind the scenes in the best way (if you don't introduce any bad practice, as mentioned, if you mess with the views IDs, for example, the framework may won't be able to perform the best action).
But even this happens in a kind of a black box2, understanding this diffing logic helps avoid common pitfalls like unexpected animations, performance hiccups, or views resetting their internal state.
Let’s now consider a slightly more dynamic scenario using ForEach
, where SwiftUI needs to reconcile a list of items that change over time:
// Initial state
struct ContentView: View {
var items = ["A", "B"]
var body: some View {
VStack {
ForEach(items, id: \.self) { item in
Text(item)
}
}
}
}
Then, the items
property is updated to:
var items = ["C", "B"] // "A" removed, "C" inserted at index 0
Visual diff:
Initial View Hierarchy: Updated View Hierarchy:
VStack VStack
├── Text("A") [ID: "A"] ├── Text("C") [ID: "C"] ← inserted
└── Text("B") [ID: "B"] └── Text("B") [ID: "B"] ← reused
Because we provided .self
as the identifier, SwiftUI can track each item’s identity based on the string value itself. It reuses "B"
, inserts "C"
, and removes "A"
— all without us writing a single line of imperative code.
UIKit comparison: manual diffing
To accomplish the same thing in UIKit, you’d typically need a UICollectionView
or UITableView
, and manually compute the diff between the old and new data. For example:
collectionView.performBatchUpdates {
// Manually calculate differences
collectionView.deleteItems(at: [IndexPath(item: 0, section: 0)])
collectionView.insertItems(at: [IndexPath(item: 0, section: 0)])
} completion: { _ in
// Completion handler
}
That’s a lot more code, and a lot more chances to get things wrong — like forgetting to update the data source or mismatching index paths.
With SwiftUI, the framework does the diffing and view recycling for you, based solely on the new state you declare. The end result is often cleaner code, better performance, and fewer bugs.
When identifiers are not stable
Imagine this example, where each item is assigned a new UUID()
on every render:
struct ContentView: View {
var items = ["A", "B"]
var body: some View {
VStack {
ForEach(items, id: \.self) { item in
Text(item)
.id(UUID()) // ⚠️ bad: forces recreation every time
}
}
}
}
Even though the data looks the same (["A", "B"]
), every time SwiftUI recreates the view state tree the elements are created with different identifiers, leading the framework understand that the state changed. Here's how the view hierarchy behaves:
First render: Second render (same data!):
VStack VStack
├── Text("A") [UUID: 123] ├── Text("A") [UUID: 456] ← new
└── Text("B") [UUID: 124] └── Text("B") [UUID: 457] ← new
This means that SwiftUI will render all the views again, impacting in animations, losing state (like focus or selection), and impacting in performance.
When identifiers are not unique
Now imagine the opposite case: the id
is stable, but not unique. This can happen when using a property like name
as the identifier, which might be shared between multiple items:
struct Person: Identifiable {
var id: String { name } // ⚠️ bad: name might not be unique
let name: String
let age: Int
}
struct ContentView: View {
let people = [
Person(name: "Alice", age: 30),
Person(name: "Bob", age: 40),
]
var body: some View {
List(people) { person in
VStack(alignment: .leading) {
Text(person.name)
Text("Age: \(person.age)")
}
}
}
}
Let’s say your data source changes — Alice is replaced by another Person
with the same name but different age:
let updatedPeople = [
Person(name: "Alice", age: 25), // new person, same name
Person(name: "Charlie", age: 35),
]
Since SwiftUI is using name
as the ID, it thinks this is still the same "Alice" and reuses the view — but the data is different. Here's what happens:
Before update: After update (wrong!)
List List
├── Alice (age: 30) [id: Alice] ├── Alice (age: 30) [id: Alice] ← ⚠️reused!
└── Bob (age: 40) [id: Bob] └── Charlie (age: 35) [id: Charlie]
Even though the second Alice is 25, the view still shows the old age (30), because the view wasn’t updated — SwiftUI didn’t detect a difference due to the reused ID.
This kind of issue can lead to:
- Outdated UI being shown (like wrong values),
-
Logic bugs if you're relying on
@State
or user interactions, - Hard-to-diagnose issues during animations or reordering.
Solution: ensure your IDs are both stable and truly unique, such as:
struct Person: Identifiable {
let id: UUID
let name: String
let age: Int
}
That way, SwiftUI correctly understands when to reuse and when to rebuild, keeping your UI accurate and your state consistent.
Runtime Render Decision
SwiftUI doesn't commit to a single rendering strategy. At runtime, it decides how each view will be rendered based on things like hardware resources (e.g., GPU availability for Metal), view complexity, and the platform itself. For example, visionOS doesn’t even offer UIKit or AppKit — so SwiftUI uses entirely different mechanisms behind the scenes.
This means that the same SwiftUI code can result in different implementations depending on the context.
Take this example:
var body: some View {
Text("Hello!")
}
Inspecting this with Xcode’s View Hierarchy tool shows that the system used a CGDrawingView
. There’s no UILabel
, no UIKit — just SwiftUI rendering directly.
Now compare that to this:
typealias Item = (id: String, title: String)
@State var data = Array(
repeating: Item(
id: UUID().uuidString,
title: "Hello!"
),
count: 1000
)
var body: some View {
VStack {
List(data, id: \.id) {
Text($0.title)
}
}
}
In this case, SwiftUI falls back to UIKit. The View Hierarchy reveals a UICollectionView
being used under the hood. The same app, in the same device, but a completely different runtime behavior. The system evaluated the size and complexity of the data and chose the most efficient path.
So that debunks the myth that SwiftUI is built on top of UIKit, and also explain why people think that: sometimes, SwiftUI can decide for using UIKit, but that is not always the case.
Conclusion
SwiftUI and UIKit are fundamentally different tools, not just in syntax but in philosophy and runtime behavior. In this article, we explored and clarified key distinctions to go beyond surface-level comparisons.
Here’s a summary of what we covered:
- The misconception that SwiftUI is just a “prettier” way to write UIKit code.
- UIKit’s imperative approach vs. SwiftUI’s declarative and reactive nature.
- The role of state management and how SwiftUI uses it to drive UI updates automatically.
- Differences in lifecycle and rendering control between UIKit and SwiftUI.
- How SwiftUI can and cannot use UIKit under the hood, and that this is decided in runtime.
Understanding these differences is key, not to have a favorite, but to correctly argue which framework is better depending on your project scenario. SwiftUI isn't just the future because it's new or cool, it's the future because it enables a more scalable way of thinking about UI. However, UIKit remains relevant and powerful, especially in contexts that demand fine-grained control. Understanding the mechanics of both frameworks empowers the developer in making a better choice.
Top comments (2)