iOS SwiftUI 2026: Construyendo Apps Modernas sin UIKit
  • ProgrammersGear · 12 Jan 2026 ·

iOS SwiftUI 2026: Construyendo Apps Modernas sin UIKit

Cómo migrar de UIKit a SwiftUI, patrones MVVM, y testing en apps complejas

SwiftUI ya no es "la opción para prototipos" que era hace unos años. Con el Observation Framework introducido en iOS 17, buena parte de los problemas clásicos de gestión de estado desaparecieron, y hoy es perfectamente viable construir apps de producción serias sin tocar UIKit. Esto es lo que cambia si vienes del modelo antiguo, y los patrones que de verdad importan cuando la app crece.

Del caos de @StateObject al Observation Framework

Antes de iOS 17, el combo @State, @StateObject, @EnvironmentObject y @Published funcionaba, pero tenía trampas invisibles: era fácil acabar con ciclos de referencia en el ViewModel sin darte cuenta.

// Antes: @StateObject retiene el ViewModel, y hay que vigilar
// los ciclos de referencia manualmente
class UserViewModel: ObservableObject {
    @Published var user: User?
}

@StateObject private var vm = UserViewModel()

// Con Observation Framework: más simple, sin @Published por campo
@Observable
final class UserViewModel {
    var user: User?
}

@State private var vm = UserViewModel()

La diferencia no es solo estética. Al usar macros y semántica de valor, el nuevo framework hace mucho más difícil crear un retain cycle por accidente, que era una de las causas más comunes de memory leaks en apps SwiftUI antiguas.

Por qué tus listas se redibujan enteras (y cómo evitarlo)

Una trampa clásica: cada vez que SwiftUI recalcula el body de una vista, recalcula también todos sus subviews. Si tienes una lista de cien elementos y solo cambia el precio de uno, pero el estado que observas está a nivel de la lista completa, los cien se redibujan.

// MAL: toda la lista observa el mismo estado
struct OrdersView: View {
    @Observable var vm: OrderViewModel

    var body: some View {
        List(vm.orders) { order in
            OrderRow(order: order, latestPrice: vm.latestPrice)
        }
    }
}

// BIEN: cada fila observa solo lo que necesita
struct OrderRow: View {
    let order: Order
    @Observable var priceVM: PriceViewModel

    var body: some View {
        HStack {
            Text(order.name)
            Text("\(priceVM.price(for: order.id))")
        }
    }
}

Separar el estado por granularidad —que cada fila observe solo su propio dato, no un objeto compartido— suele ser la diferencia entre una lista fluida y una que se nota "a saltos" cuando llegan datos en tiempo real.

Navegación en apps grandes: cuándo NavigationStack se queda corto

Para apps pequeñas, el NavigationStack por defecto de SwiftUI es suficiente. Cuando la app crece y necesitas deep linking desde notificaciones, historial de navegación persistente, o poder testear un flujo sin simular taps en la UI, conviene introducir un Router propio:

@Observable
final class Router {
    var navigationStack: [Route] = []

    func navigate(to route: Route) {
        navigationStack.append(route)
    }

    func goBack() {
        navigationStack.removeLast()
    }
}

enum Route: Hashable {
    case userProfile(id: String)
    case orderDetails(orderId: String)
    case payment(amount: Decimal)
}

Con esto puedes escribir tests del tipo router.navigate(to: .userProfile(id: "123")) y comprobar que la UI muestra el perfil correcto, sin necesidad de interactuar con la pantalla.

Memory leaks: el clásico closure que retiene self

Buena parte de los crashes relacionados con memoria en apps SwiftUI vienen de closures que capturan self fuerte dentro de una tarea async:

// Peligro: la Task retiene self, y self referencia la task indirectamente
.onAppear {
    Task {
        let result = await vm.fetchData()
        self.data = result
    }
}

// Más seguro: captura débil
.onAppear {
    Task { [weak self] in
        guard let self else { return }
        let result = await self.vm.fetchData()
        self.data = result
    }
}

No siempre hace falta [weak self] —si la tarea vive exactamente lo mismo que la vista, no pasa nada— pero en tareas de larga duración o que sobreviven a la vista, es la diferencia entre un leak silencioso y código correcto.

Herramientas útiles

Xcode trae un profiler con una vista específica para SwiftUI que muestra en tiempo real qué vistas se están redibujando; es la forma más directa de confirmar si un cambio de arquitectura realmente redujo los rerenders.

Instruments, incluido con Xcode, es la herramienta de referencia para detectar frame drops con su módulo de Core Animation.

Charles Proxy (de pago) sigue siendo útil para depurar peticiones de red durante el desarrollo.

En resumen

Si tu app SwiftUI todavía usa el patrón previo a iOS 17, hay margen de mejora sin necesidad de reescribir nada: adoptar el Observation Framework, separar el estado por granularidad, e introducir un Router si la navegación lo pide. Para una app de tamaño medio, esa migración suele llevar un par de días de trabajo y el resultado se nota, sobre todo en pantallas con actualizaciones frecuentes.

Fuentes:

iosswiftswiftuimobiledesarrollo
📢 SmartAd Placeholder (in-article)
Volver a la página principal

Comentarios (0)

Deja un comentario

No hay comentarios aún. ¡Sé el primero en comentar!

Instalar ProgrammersGear

Accede a tu contenido favorito directamente desde tu pantalla de inicio