UIKit vs SwiftUI — ¿en qué casos seguiría un ingeniero senior de iOS eligiendo UIKit en 2025?
SwiftUI es la opción por defecto para pantallas nuevas, pero las apps en producción todavía dependen de UIKit para listas densas, control preciso de texto e interoperabilidad con código legado.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
UIKit vs SwiftUI — ¿en qué casos seguiría un ingeniero senior de iOS eligiendo UIKit en 2025?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.Esta pregunta funciona como filtro, no como trivia. Para 2025 SwiftUI ya lleva seis años en el mercado, sumó NavigationStack, el framework de Observation, Swift Charts y un motor de diffing bastante más rápido, y Apple lo empuja como la opción por defecto para pantallas nuevas en todas las plataformas donde corre. Un candidato que responde "usá SwiftUI para todo, siempre" probablemente nunca lanzó una pantalla con cincuenta mil filas, un editor de texto enriquecido o un SDK de cámara de terceros. Uno que responde "UIKit es más maduro, yo solo uso eso" probablemente no tocó un código base armado después de 2021. La respuesta que muestra experiencia real está en el medio: saber con precisión qué capacidades todavía inclinan la balanza hacia UIKit, y entender que la habilidad real es combinar los dos frameworks, no elegir un bando.
La diferencia de modelo mental importa más que la de sintaxis. SwiftUI es declarativo: el body de una vista es una función pura que va de un estado a una descripción de la interfaz, y el motor de diffing del framework decide qué hace falta cambiar en pantalla. UIKit es imperativo: tenés una referencia a un UILabel y le decís label.text = "Nuevo" en el momento exacto en que querés actualizarlo. No es solo una cuestión de estilo — cambia la forma de los bugs que terminás debuggeando. Un bug de UIKit suele ser "me olvidé de empujar un cambio a la vista X cuando cambió el modelo". Un bug de SwiftUI suele ser "mi modelo de estado dejó que dos propiedades queden en desacuerdo entre sí", porque una vez que el estado es correcto, el renderizado está garantizado que lo va a reflejar solo.
Aun así, hay lugares concretos donde UIKit sigue siendo la herramienta más fuerte, y un ingeniero senior tiene que poder nombrarlos sin dudar:
- Listas densas, heterogéneas y de gran volumen.
UICollectionViewCompositionalLayoutcombinado con un diffable data source todavía da un control por celda de tamaño, prefetching y reuso queListyLazyVStackaproximan pero no igualan — sobre todo pasadas unas pocas miles de filas con tipos de celda mixtos, donde el costo del diffing por identidad de SwiftUI se nota como trabas en el scroll. - Edición de texto precisa.
UITextViewrespaldado porNSAttributedStringy TextKit le sigue ganando aTextEditorcuando necesitás adjuntos de texto personalizados, menciones inline o una vista accesoria de entrada a medida — pensá en compositores de chat o editores de markdown. - Animación interrumpible, guiada por física.
UIViewPropertyAnimatorte deja arrastrar, pausar y revertir una animación a mitad de camino a partir de un delta de touch crudo, que es exactamente el control que necesita un bottom sheet o una tarjeta que se arrastra. El sistema de gestos y animación de SwiftUI cerró buena parte de esa brecha, pero lograr un drag interrumpible perfecto sigue siendo más predecible en UIKit. - Cámara, mapas y SDKs de terceros.
AVCaptureVideoPreviewLayer, unMKMapViewmuy personalizado, la mayoría de los SDKs de pago o de escaneo de documentos — todos estos vienen comoUIViewController, y los envolvés en vez de reimplementarlos. - Soportar un deployment target viejo. Si la app todavía soporta iOS 15 y el diseño pide un modifier que recién llegó en iOS 17, UIKit no es una preferencia, es la única opción disponible.
La habilidad realmente senior es la interoperabilidad, no la exclusividad. UIHostingController mete un árbol de SwiftUI dentro de un stack de navegación de UIKit; UIViewRepresentable y UIViewControllerRepresentable meten una vista o un controlador de UIKit dentro de un árbol de SwiftUI, coordinados por un objeto Coordinator que traduce los callbacks del delegate al mundo de SwiftUI. La mayoría de las pantallas reales de una app madura terminan siendo una cáscara de SwiftUI envolviendo una o dos islas de UIKit, no un espécimen puro de ninguno de los dos frameworks.
Así se ve en la práctica — un feed paginado que se mantiene sobre UICollectionViewDiffableDataSource por rendimiento de scroll, envuelto para que el resto de la app lo trate como una vista de SwiftUI cualquiera:
import SwiftUIimport UIKit/// Envuelve un feed basado en UICollectionView (que se mantiene en UIKit/// por el rendimiento del compositional layout + diffable data source)/// dentro de una pantalla de SwiftUI.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}
Cuando esto sale en una entrevista, las mejores respuestas citan una falla concreta en vez de una regla general: un LazyVStack que mantenía viva la instancia de cada fila sin reuso y hacía crecer la memoria sin límite en un feed lleno de imágenes; un TextEditor que perdía la posición del cursor con actualizaciones programáticas de texto antes de iOS 16; un List cuyas filas trababan porque no tenían claves Identifiable estables y baratas. Nombrar la falla, y no solo la regla, es lo que separa "leí las release notes" de "me pasó en producción".
Una forma rápida de organizar la decisión en el momento: