UIKit vs SwiftUI — when would a senior iOS engineer still reach for UIKit in 2025?
SwiftUI is the default for new screens, but production apps still lean on UIKit for dense scrolling lists, precise text control, and legacy interop — knowing exactly when signals real production mileage.
LEARN BY DOINGPut your intuition to work3 steps
UIKit vs SwiftUI — when would a senior iOS engineer still reach for UIKit in 2025?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.This question works as a filter, not a trivia check. By 2025, SwiftUI has shipped for six years, picked up NavigationStack, the Observation framework, Swift Charts, and a much faster diffing engine, and Apple pushes it as the default for new screens on every platform it ships to. A candidate who answers with "just use SwiftUI for everything" has probably never shipped a screen with fifty thousand rows, a rich-text composer, or a third-party camera SDK. A candidate who answers "UIKit is more mature, I only use that" has probably not touched a codebase built after 2021. The answer that actually signals seniority sits in the middle: know exactly which capabilities still tip the balance toward UIKit, and know that the real skill is composing the two frameworks together, not picking a side.
The mental-model split matters more than the syntax difference. SwiftUI is declarative: a view's body is a pure function from state to a description of the UI, and the framework's own diffing engine decides what actually needs to change on screen. UIKit is imperative: you hold a reference to a UILabel and you tell it label.text = "New" the moment you want it updated. This is not just a style preference — it changes the shape of the bugs you end up debugging. A UIKit bug is usually "I forgot to push a change to view X after the model changed." A SwiftUI bug is usually "my state model let two properties disagree with each other," because once state is correct, the rendering is guaranteed to catch up on its own.
Even so, there are concrete places where UIKit remains the stronger tool, and a senior engineer should be able to name them without hesitating:
- Dense, heterogeneous, high-volume lists.
UICollectionViewCompositionalLayoutpaired with a diffable data source still gives cell-level control over sizing, prefetching and reuse thatListandLazyVStackapproximate rather than match — especially past a few thousand rows with mixed cell types, where SwiftUI's identity-diffing overhead becomes visible as scroll hitches. - Precise text editing.
UITextViewbacked byNSAttributedStringand TextKit still beatsTextEditorwhen you need custom text attachments, inline mentions, or a custom input accessory view — think chat composers or markdown editors. - Interruptible, physics-driven animation.
UIViewPropertyAnimatorlets you scrub, pause and reverse an animation mid-flight from a raw touch delta, which is exactly the control a custom bottom sheet or card-drag interaction needs. SwiftUI's gesture and animation system has closed much of this gap, but getting an interruptible drag exactly right is still more predictable in UIKit. - Camera, maps and third-party SDKs.
AVCaptureVideoPreviewLayer, a heavily customizedMKMapView, most payment and document-scanning SDKs — these ship asUIViewControllers, and you wrap them rather than reimplement them. - Supporting an older minimum deployment target. If the app still supports iOS 15 and the design calls for a modifier that only landed in iOS 17, UIKit is not a preference, it is the only option available.
The actual senior-level skill is interop, not exclusivity. UIHostingController drops a SwiftUI tree into a UIKit navigation stack; UIViewRepresentable and UIViewControllerRepresentable drop a UIKit view or controller into a SwiftUI tree, coordinated through a Coordinator object that bridges delegate callbacks back into SwiftUI's world. Most real screens in a mature app end up as a SwiftUI shell wrapping one or two UIKit islands, not a pure specimen of either framework.
Here is what that looks like in practice — a paginated feed kept on UICollectionViewDiffableDataSource for scroll performance, wrapped so the rest of the app can treat it as an ordinary SwiftUI view:
import SwiftUIimport UIKit/// Wraps a UICollectionView-based feed (kept in UIKit for compositional/// layout + diffable data source performance) inside a SwiftUI screen.struct FeedCollectionView: UIViewControllerRepresentable {let items: [FeedItem]let onSelect: (FeedItem) -> Voidfunc makeUIViewController(context: Context) -> FeedViewController {let controller = FeedViewController()controller.onSelect = onSelectreturn controller}func updateUIViewController(_ controller: FeedViewController, context: Context) {controller.apply(items: items)}}final class FeedViewController: UIViewController {var onSelect: ((FeedItem) -> Void)?private var dataSource: UICollectionViewDiffableDataSource<Int, FeedItem>!private lazy var collectionView: UICollectionView = {let layout = UICollectionViewCompositionalLayout.list(using: UICollectionLayoutListConfiguration(appearance: .plain))return UICollectionView(frame: .zero, collectionViewLayout: layout)}()override func viewDidLoad() {super.viewDidLoad()view.addSubview(collectionView)collectionView.frame = view.boundscollectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]collectionView.delegate = selflet cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, FeedItem> {cell, _, item invar config = cell.defaultContentConfiguration()config.text = item.titlecell.contentConfiguration = config}dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {collectionView, indexPath, item incollectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)}}func apply(items: [FeedItem]) {var snapshot = NSDiffableDataSourceSnapshot<Int, FeedItem>()snapshot.appendSections([0])snapshot.appendItems(items)dataSource.apply(snapshot, animatingDifferences: true)}}extension FeedViewController: UICollectionViewDelegate {func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {guard let item = dataSource.itemIdentifier(for: indexPath) else { return }onSelect?(item)}}struct FeedItem: Hashable {let id: UUIDlet title: String}
When this comes up in an interview, the strongest answers reference a specific failure mode rather than a rule of thumb: a LazyVStack that kept every row's view instance alive with no reuse and grew memory unbounded on an image-heavy feed; a TextEditor that lost cursor position on programmatic text updates before iOS 16; a List whose rows stuttered because they lacked stable, cheap Identifiable keys. Naming the failure, not just the rule, is what separates "I read the release notes" from "I have hit this in production."
A quick way to organize the decision on the spot: