Search
🔮

Can SwiftUI Replace UIKit?

생성일
2024/12/14 11:16
태그

Can SwiftUI Replace UIKit?

3-Sentence Summary

• The combination of SwiftUI and TCA (The Composable Architecture) provides the most elegant way to utilize Swift’s Concurrency.
• The declarative UI nature of SwiftUI is an incredibly powerful advantage but can also be a significant drawback.
• I believe it is still nearly impossible to develop a service entirely using SwiftUI alone.

Can SwiftUI Replace UIKit?

Since August 2023, I have been developing services using SwiftUI at NHN FashionGo Korea. While building new features with SwiftUI on an existing UIKit-based app, we are gradually transitioning the UIKit-based screens to SwiftUI.
This shift to SwiftUI was a strategic decision to adopt Apple’s latest technology stack, simplify code, and improve maintainability. However, during this process, I’ve experienced moments of awe, frustration, and even despair, which helped me clearly understand the differences between SwiftUI and UIKit.
In this article, I aim to share the specific challenges I faced while adopting SwiftUI, how I tried to overcome them, and my conclusion on whether SwiftUI can truly replace UIKit. I hope this will be helpful for developers debating between SwiftUI and UIKit for their projects.

Advantages of SwiftUI

1. Enhanced Productivity
One of the most significant advantages I’ve experienced with SwiftUI is its enhanced productivity. Compared to UIKit, the amount of code required is drastically reduced, and defining the UI inherently translates into defining the layout itself. This is made possible by SwiftUI’s declarative syntax. Since auto-layout-related code is unnecessary in SwiftUI, development time is significantly shortened.
Here’s a simple example of arranging three buttons using both SwiftUI and UIKit. With SwiftUI, the UI and layout can be defined simultaneously with the following code:
struct ContentView: View { var body: some View { VStack(spacing: 16) { Button("Button 1") .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) Button("Button 2") .background(Color.green) .foregroundColor(.white) .cornerRadius(8) Button("Button 3") .background(Color.red) .foregroundColor(.white) .cornerRadius(8) } .frame(maxWidth: .infinity, maxHeight: .infinity) } }
Swift
복사
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.gray.withAlphaComponent(0.2) // 버튼 생성 let button1 = createButton(title: "Button 1", backgroundColor: .blue) let button2 = createButton(title: "Button 2", backgroundColor: .green) let button3 = createButton(title: "Button 3", backgroundColor: .red) // 버튼 스택 뷰 생성 let stackView = UIStackView(arrangedSubviews: [button1, button2, button3]) stackView.axis = .vertical stackView.spacing = 16 stackView.translatesAutoresizingMaskIntoConstraints = false // 스택 뷰 추가 view.addSubview(stackView) // 스택 뷰 레이아웃 NSLayoutConstraint.activate([ stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) } private func createButton(title: String, backgroundColor: UIColor) -> UIButton { let button = UIButton(type: .system) button.setTitle(title, for: .normal) button.backgroundColor = backgroundColor button.setTitleColor(.white, for: .normal) button.layer.cornerRadius = 8 button.translatesAutoresizingMaskIntoConstraints = false button.heightAnchor.constraint(equalToConstant: 44).isActive = true return button } }
Swift
복사
2. The Most Elegant Structure for Utilizing Swift’s Concurrency
Before using SwiftUI, I preferred a codebase built with the MVVM + RxSwift combination. RxSwift processes asynchronous tasks in the form of streams and controls the flow and timing of events using Observable operators. However, introducing Apple’s Concurrency into an MVVM + RxSwift codebase is not ideal. RxSwift (or Combine) already manages asynchronous execution contexts with operators like ObserveOn and SubscribeOn, and adding Concurrency would only increase complexity through redundancy.
The most widely adopted (and essentially the default) architecture in SwiftUI is TCA (The Composable Architecture). Initially, I was hesitant about relying on a third-party library rather than an Apple-first framework for app architecture. However, I now believe TCA offers the most ideal integration of SwiftUI’s State-Driven UI and Apple’s Concurrency.
TCA’s Reducer utilizes Task to handle various business logic. For example, fetching data through API calls or processing complex asynchronous tasks can be done without nested closures, resulting in concise and clean code. With await/async, asynchronous tasks can be expressed clearly, and Reducers ensure that logic and state changes are well-defined, making them easy to test.
Feature
MVVM + RxSwift
SwiftUI + TCA
Code Complexity
Increased complexity due to RxSwift operators
Clear logic separation through Reducer
Async Handling
Requires operator chains like flatMapLatest
Utilizes Task, await/async
State Management
Difficult to track state changes
Managed through explicit State and Action
UI Updates
Requires binding Observable to View
State and SwiftUI views automatically sync
Here’s an example of how TCA handles actions in its reduce function and uses Swift’s async to request permissions from PHPhotoLibrary:
func reduce(into state: inout State, action: Action) -> Effect<Action> { switch action { case .requestPermission: return requestPhotoPermission() } } func requestPhotoPermission() -> Effect<Action> { return Effect.run { send in let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite) await send(.permissionGranted(status == .authorized || status == .limited)) } }
Swift
복사
This example demonstrates how TCA defines asynchronous tasks clearly using Task, separating state changes from business logic. By doing so, TCA makes it easier to manage asynchronous workflows, ensuring logic is cleanly defined and testable. Additionally, it avoids nested closures, improving code readability and enabling developers to write intuitive, maintainable code in tandem with declarative UI.

Real-World Limitations of SwiftUI

1. Neither Fully UIKit Nor Fully SwiftUI
Although SwiftUI appears to be a completely new framework independent of UIKit, many of its Views are actually wrappers around UIKit components. For instance, the List in SwiftUI behaves differently depending on the iOS version. It relies on UITableView for iOS 14/15 and transitions to UICollectionView in iOS 16 and later. This means that the behavior of a SwiftUI View can vary significantly across iOS versions.
This discrepancy has caused real-world issues. Starting with iOS 15, both UITableView and UICollectionView introduced a sectionHeaderTopPadding property with a default value, leading to unremovable empty padding—even when headers weren’t used. Similarly, SwiftUI’s List inexplicably added padding, which developers were forced to remove. Unfortunately, List in SwiftUI doesn’t provide an option to set sectionHeaderTopPadding. To resolve this issue, it was necessary to drop down to the UIKit level:
List {} .introspect(.list, on: .iOS(.v15)) { tableView in tableView.sectionHeaderTopPadding = 0 } .introspect(.list, on: .iOS(.v16, .v17, .v18)) { collectionView in collectionView.sectionHeaderTopPadding = 0 }
Swift
복사
2. ScrollView + Dynamic Content
SwiftUI’s declarative UI model struggles in scenarios where content changes dynamically, especially when dealing with ScrollViews, dynamic cells, and pagination.
* Limitations in Pagination
Pagination involves fetching additional data and appending it to either the beginning or end of existing content. With UIKit’s UITableView, the DataSource efficiently handles data additions, updating only the necessary cells and maintaining scroll position seamlessly. For example, when prepending data (prev pagination), the user’s current view remains intact.
However, with SwiftUI’s List, any change to the bound data is treated as a state change, which triggers a full re-render of the List. This behavior causes the scroll position to reset or shift during prev pagination, leading to a disorienting user experience. Such issues significantly degrade UX and make pagination scenarios challenging to implement cleanly in SwiftUI.
Prev Pagination in UIKit.
Prev Pagination in SwiftUI.
When the user scrolls up and pagination occurs, the data does not naturally continue from 91 to 90. Instead, an awkward situation arises where 81 appears immediately after 91.
ScrollViewReader { proxy in ScrollView { LazyVStack { ForEach(items, id: \.self) { item in Text("Item \(item)") .padding() .id(item) } } } .onChange(of: items) { _ in if let lastViewedItem = lastViewedItemId { proxy.scrollTo(lastViewedItem, anchor: .top) } } }
Swift
복사
To address this issue, ScrollViewReader can be used to track the id of each cell. After adding new data, the scrollTo function can move to the id of the last viewed cell, achieving somewhat similar functionality. However, even this approach does not work seamlessly. It behaves unnaturally and, while it functions in iOS 17, it does not work at all in iOS 18.
3. Bugs in SwiftUI
1.
setNavigationHidden
SwiftUI comes with its fair share of bugs. For instance, SwiftUI provides a modifier called setNavigationHidden() to hide the NavigationBar.
However, in iOS 15, there was an intermittent issue where the NavigationBar wouldn’t hide properly. To resolve this, I had to add additional handling at the HostingViewController lifecycle level.
2.
CollectionView
In iOS 17, a bug appeared when displaying a List with two columns and only one item (leaving the right side empty). This caused the scroll to bounce unexpectedly. To address this, I had to add a single invisible dummy cell for lists with one item—an issue specific to iOS 17.
3.
Reorder Functionality in UITableView and UICollectionView
UIKit’s UITableView and UICollectionView provide content reordering features in EditMode. SwiftUI also offers a similar feature for reordering content, but it exhibits different bugs depending on the iOS version:
• Data updates successfully, but the UI does not reflect the changes.
• The last item cannot be dragged.
• Even when reordering is disabled for certain cells, the reorder action still works.
Different versions of iOS introduced different bugs, making this feature frustrating to work with.
4.
Other Version-Specific Issues
Beyond the above examples, I encountered numerous other bugs. The most challenging aspect was that these bugs varied by iOS version, making them difficult to identify during the QA process. Even when discovered, finding satisfactory solutions was often just as challenging.

Why It’s Impossible to Develop Entirely with SwiftUI

1. Features Not Yet Available in SwiftUI
It’s unclear whether Apple hasn’t developed these features yet, or if this is intentional, but certain functionalities like WKWebView (WebKit) and Photo libraries are still unavailable in SwiftUI.
Given the complexity of user interactions and UI behavior in WebView and Photo-related tasks, imperative programming is arguably better suited for these scenarios than declarative UI.
2. Continuous Changes to SwiftUI Views (Minimum Supported Version Issues)
SwiftUI is still incomplete. Even years after its release, new features are continuously being added. Frankly, it’s surprising that some of these features weren’t available from the beginning—for instance, the fact that Pull To Refresh was only introduced in iOS 15 is shocking.
Moreover, critical navigation logic for iOS apps only became available starting with iOS 16.
These are not just minor features but fundamental changes that are essential for developing service-oriented apps. Naturally, newly added features are tied to the latest iOS versions for their minimum support.
Since most services are already live, we can’t afford to abandon existing users. This leaves us in a position where we have to wait and watch before adopting these new features.

Using SwiftUI and UIKit Together

Unless starting a brand-new project with SwiftUI from scratch, it’s more common to gradually transition an existing UIKit-based service to SwiftUI.
Apple provides tools to support this process, including UIViewRepresentable, UIViewControllerRepresentable, and HostingViewController.
These tools allow developers to wrap UIKit’s UIView and UIViewController into SwiftUI-compatible Views using UIViewRepresentable and UIViewControllerRepresentable. Conversely, HostingViewController makes it possible to embed SwiftUI Views within UIKit environments. It’s feasible, but… not perfect.
Differences in State Management
SwiftUI uses state-driven updates, whereas UIKit follows an imperative update model. This difference can make data synchronization between the two frameworks challenging.
Timing Differences in View Updates
The timing for updating the UI can differ between UIKit and SwiftUI, potentially causing unexpected bugs.
Additional Complexity
Using both frameworks together increases code complexity, making maintenance more difficult over time.

Will UIKit Eventually Disappear Like Objective-C?

While it’s impossible to predict Apple’s future decisions with certainty, I personally believe that UIKit will remain relevant for a long time. Although SwiftUI is a framework full of potential, it still lacks the stability and flexibility that UIKit has long provided.
1. A Parallel from the Past: Storyboard vs. Code-Based UI
The relationship between SwiftUI and UIKit reminds me of the earlier comparison between Storyboard and code-based UI. When Storyboard was first introduced, many developers believed it would eliminate the need to write code for UI design. Yet, today, the majority of app development still relies heavily on code-based UI.
Storyboard gradually fell out of favor due to the following reasons:
• It made collaboration and version control extremely difficult in large-scale projects.
• It imposed significant limitations on creating complex or dynamic UIs.
Similarly, while SwiftUI has brought innovation with its declarative UI approach, it still lacks the proven stability and flexibility of UIKit.
2. Why SwiftUI May Not Fully Replace UIKit
There are several reasons why SwiftUI is unlikely to completely replace UIKit:
Lack of Stability
• As a relatively young framework, SwiftUI still suffers from numerous bugs and inconsistencies across iOS versions.
• Introducing SwiftUI into existing UIKit-based apps often requires using both frameworks together, which adds complexity.
Insufficient Flexibility
• UIKit has been a well-established framework for over a decade, capable of handling almost every UI requirement with great flexibility.
• In contrast, SwiftUI still lacks support for advanced features like WebView and Photo libraries, which are often essential in real-world applications.
The Pace of Ecosystem Changes
• SwiftUI evolves rapidly, with new features added and APIs changed every year. While this is promising in the long term, it creates challenges for maintaining minimum supported versions in production environments, making it difficult to rely solely on SwiftUI for projects.

Points to Consider Before Using SwiftUI in a Project

1. Flexibility in Design and Planning
SwiftUI has inherent limitations. Advanced UI interactions (e.g., custom layouts, complex animations) can be harder or even impossible to implement compared to UIKit. For this reason, projects using SwiftUI must ensure designs and plans are adaptable to the constraints of SwiftUI.
2. Availability of a Design System
If your project already has a UIKit-based design system, transitioning to SwiftUI requires making a critical decision:
1.
Recreating the Design System for SwiftUI
• A SwiftUI-specific design system fully leverages SwiftUI’s declarative paradigm.
• However, it incurs significant redevelopment costs and makes the initial transition complex.
2.
Reusing the UIKit Design System via UIViewRepresentable
• This allows the existing UIKit design system to be reused in SwiftUI, saving initial costs.
• However, it can result in more complex code and does not align perfectly with SwiftUI’s declarative nature.
3. If Creating a New Design System
1.
Building with UIKit
• UIKit-based systems work across both UIKit and SwiftUI, but this undermines the value of transitioning to SwiftUI.
2.
Building with SwiftUI
• A SwiftUI-based system is limited to SwiftUI and might require redevelopment if UIKit becomes necessary for future customizations.
4. Importance of Maintainability
As previously mentioned, SwiftUI is far from perfect. Its behavior varies across iOS versions, and minor bugs can require multiple hotfixes.
When customizations involve using Introspect to access UIKit-level features, the added maintenance burden grows with each new iOS version.
SwiftUI is suitable for projects that involve daily development and regular maintenance. However, for one-off projects that are developed and then only maintained minimally, SwiftUI is not recommended.
“End”