SwiftUI: Construccion de Interfaces Modernas para iOS 19
Guia completa para crear interfaces modernas con SwiftUI: sintaxis declarativa, componentes, animaciones y mejores practicas para iOS 19.

SwiftUI ha transformado el desarrollo de interfaces en las plataformas Apple. Con su sintaxis declarativa y su integracion nativa, este framework permite construir aplicaciones elegantes con menos lineas de codigo que nunca. iOS 19, lanzado en la WWDC 2026, trae mejoras importantes: @State ahora es una macro con inicializacion diferida, las swipe actions funcionan en cualquier vista, y las nuevas API de contenedores reordenables simplifican el arrastrar y soltar en todas las plataformas.
SwiftUI alcanza su madurez con iOS 19, ofreciendo el diseno Liquid Glass que moderniza automaticamente las aplicaciones existentes, inicializacion @State optimizada, e integracion simplificada con UIKit. Se ha consolidado como el estandar para nuevas aplicaciones iOS.
Comprendiendo el Paradigma Declarativo
Antes de escribir codigo, es fundamental entender lo que diferencia a SwiftUI. Con UIKit (el framework anterior), era necesario indicarle a iOS como construir la interfaz paso a paso: "crea una etiqueta, colocala aqui, cambia su color cuando el usuario toque". Ese es el paradigma imperativo.
SwiftUI funciona de manera distinta: se describe que se desea mostrar y el framework se encarga del resto. Es como la diferencia entre dar indicaciones paso a paso con un GPS (imperativo) y simplemente indicar el destino (declarativo).
La Primera Vista
En SwiftUI, cada elemento de interfaz es un View. Un View es un struct que describe lo que debe aparecer en pantalla. A continuacion, una primera pantalla con titulo, subtitulo y boton:
import SwiftUI
// Each screen is a struct that implements the View protocol
struct ContentView: View {
// The "body" property describes what the view displays
// "some View" means "a type of View, but Swift figures it out automatically"
var body: some View {
// VStack = "Vertical Stack": stacks elements vertically
// spacing: 20 = 20 points of space between each element
VStack(spacing: 20) {
// A simple text with style modifiers
Text("Welcome to SharpSkill")
.font(.largeTitle) // Large title font
.fontWeight(.bold) // Bold text
Text("Prepare for your iOS interviews")
.font(.subheadline) // Smaller font
.foregroundColor(.secondary) // Gray secondary color
// Button with an action (closure) and a label
Button("Get Started") {
print("Button tapped!")
}
.buttonStyle(.borderedProminent) // Filled blue button style
}
.padding() // Adds margins around the VStack
}
}La estructura es clara: se declaran los elementos deseados (textos, boton), se apilan verticalmente (VStack) y se aplican estilos mediante modificadores (.font(), .padding()).
Punto clave: En SwiftUI no se "crean" objetos de UI, sino que se describe la interfaz deseada. SwiftUI se encarga de crear, actualizar y destruir los elementos reales.
Modificadores: Transformando las Vistas
Los modificadores son metodos que se encadenan despues de una vista para transformarla. Funcionan de manera similar a los filtros de Instagram aplicados uno tras otro. El orden importa, ya que cada modificador crea una nueva vista que envuelve a la anterior.
El siguiente ejemplo ilustra por que el orden es crucial:
// Example 1: padding THEN background
Text("SwiftUI")
.padding() // 1. Adds 16pt of space around the text
.background(.blue) // 2. Blue background covers text + padding
.foregroundColor(.white)
// Result: white text on blue rectangle with margins
// Example 2: background THEN padding (reversed order!)
Text("SwiftUI")
.background(.blue) // 1. Tight blue background around text only
.padding() // 2. Transparent padding around blue background
.foregroundColor(.white)
// Result: white text on small blue rectangle, surrounded by empty spaceLa diferencia radica en que, en el primer caso, el padding queda "dentro" del fondo. En el segundo, queda "fuera". Un detalle sutil pero esencial para dominar los layouts.
Organizacion de Interfaces con Stacks
SwiftUI ofrece tres contenedores principales para organizar las vistas. Se pueden pensar como cajas que ordenan su contenido de maneras diferentes.
VStack, HStack y ZStack
- VStack (Vertical Stack): apila elementos de arriba hacia abajo
- HStack (Horizontal Stack): alinea elementos de izquierda a derecha
- ZStack (Z-axis Stack): superpone elementos uno sobre otro
El siguiente ejemplo construye una tarjeta de perfil de usuario combinando los tres tipos de stacks. El objetivo: mostrar una foto con insignia de verificacion, luego el nombre y rol del usuario.
struct ProfileCard: View {
var body: some View {
// Main HStack: photo on left, info on right
HStack(spacing: 16) {
// ZStack to overlay the badge on the photo
ZStack(alignment: .bottomTrailing) {
// Profile image (circle)
Image("avatar")
.resizable()
.frame(width: 60, height: 60)
.clipShape(Circle())
// Green "verified" badge at bottom right
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.background(Circle().fill(.white)) // White circle behind
}
// VStack to stack name and role vertically
VStack(alignment: .leading, spacing: 4) {
Text("Marie Dupont")
.font(.headline)
Text("iOS Developer")
.font(.subheadline)
.foregroundColor(.secondary)
}
// Spacer pushes everything to the left
Spacer()
// Chevron on right indicates it's tappable
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
}El truco esta en Spacer(): es un elemento invisible que ocupa todo el espacio disponible. Sin el, los elementos quedarian centrados. Con el, se empujan hacia la izquierda y el chevron permanece anclado a la derecha.
Usar command + click sobre cualquier vista en Xcode permite acceder al inspector visual. Se pueden agregar modificadores sin necesidad de escribir codigo.
Gestion del Estado con @State y @Binding
La gestion del estado es el corazon de SwiftUI. El estado es cualquier dato que puede cambiar y que debe actualizar la interfaz. Cuando el estado cambia, SwiftUI recalcula automaticamente las vistas afectadas.
@State: El Estado Local de una Vista
@State es un property wrapper que le indica a SwiftUI: "observa esta variable y refresca la vista cuando cambie". Es ideal para el estado local de una vista individual.
Actualizacion iOS 19: @State ahora es una macro con inicializacion diferida. En versiones anteriores, una nueva instancia de clase podia crearse cada vez que se inicializaba una vista. Ahora SwiftUI crea el objeto solo cuando se necesita por primera vez, eliminando creaciones de objetos innecesarias. Este cambio esta retroportado a iOS 17, por lo que las aplicaciones existentes se benefician automaticamente.
El siguiente contador interactivo permite comprender el mecanismo:
struct CounterView: View {
// @State creates a "source of truth" for this view
// private because state shouldn't be modified from outside
@State private var count = 0
var body: some View {
VStack(spacing: 30) {
// This Text updates automatically when count changes
Text("\(count)")
.font(.system(size: 72, weight: .bold))
HStack(spacing: 40) {
// Decrement button
Button(action: {
count -= 1 // Modifies state → view refreshes
}) {
Image(systemName: "minus.circle.fill")
.font(.largeTitle)
}
// Increment button
Button(action: {
count += 1
}) {
Image(systemName: "plus.circle.fill")
.font(.largeTitle)
}
}
}
}
}Al tocar un boton, count cambia. SwiftUI detecta este cambio y vuelve a ejecutar body para actualizar la pantalla. No es necesario actualizar manualmente la etiqueta.
@Binding: Compartiendo Estado entre Vistas
En ocasiones, una vista hija necesita modificar el estado de su vista padre. Para eso existe @Binding: crea una conexion bidireccional con un @State existente.
Un ejemplo concreto: un campo de entrada de nombre de usuario con validacion en tiempo real.
// Parent view: owns the state
struct SignupForm: View {
@State private var username = "" // Source of truth
@State private var isValid = false // Validation state
var body: some View {
VStack(spacing: 20) {
// Pass BINDINGS (with $) to the child view
UsernameField(username: $username, isValid: $isValid)
Button("Create Account") {
// Submit the form
}
.disabled(!isValid) // Disabled if invalid
.buttonStyle(.borderedProminent)
}
.padding()
}
}La vista hija recibe bindings y puede modificarlos:
// Child view: receives and modifies state via @Binding
struct UsernameField: View {
@Binding var username: String // Connection to parent's @State
@Binding var isValid: Bool
var body: some View {
VStack(alignment: .leading, spacing: 8) {
TextField("Username", text: $username)
.textFieldStyle(.roundedBorder)
.onChange(of: username) { oldValue, newValue in
// Validation: at least 3 characters
isValid = newValue.count >= 3
}
// Visual feedback
HStack {
Image(systemName: isValid ? "checkmark.circle" : "xmark.circle")
Text("Minimum 3 characters")
}
.font(.caption)
.foregroundColor(isValid ? .green : .red)
}
}
}Cuando el usuario escribe en el TextField, username se modifica a traves del binding. La vista padre detecta este cambio y puede utilizarlo. Es comunicacion bidireccional limpia.
Utilizar @State unicamente para estado simple y local. Para datos compartidos entre multiples pantallas o logica compleja, es preferible usar @Observable (iOS 17+) o patrones MVVM. Para una comparacion detallada, consultar SwiftUI @Observable vs @State: cuando usar cual.
¿Listo para aprobar tus entrevistas de iOS?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Visualizacion de Listas Dinamicas
Las listas estan presentes en practicamente toda aplicacion movil. SwiftUI proporciona List para mostrar colecciones de datos con estilos nativos de iOS (separadores, acciones de deslizamiento, etc.).
Creacion de una Lista Simple
Para mostrar una lista se necesitan dos cosas: datos y una forma de identificarlos. El protocolo Identifiable le permite a SwiftUI saber que vista corresponde a que dato.
Primero, la definicion del modelo de datos:
// Identifiable lets SwiftUI track each element
struct Interview: Identifiable {
let id = UUID() // Auto-generated unique identifier
let technology: String
let difficulty: String
let questionCount: Int
}Ahora la lista. La idea es iterar sobre los datos con ForEach y crear una fila para cada elemento:
struct InterviewListView: View {
// Data to display (in reality, this would come from an API)
@State private var interviews = [
Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25),
Interview(technology: "Android", difficulty: "Advanced", questionCount: 30),
Interview(technology: "React", difficulty: "Beginner", questionCount: 20)
]
var body: some View {
// NavigationStack enables the navigation bar
NavigationStack {
List {
// ForEach iterates over each interview
// Thanks to Identifiable, no need to specify id:
ForEach(interviews) { interview in
InterviewRow(interview: interview)
}
}
.navigationTitle("My Interviews")
}
}
}Y la vista para cada fila, extraida en su propio componente por claridad:
struct InterviewRow: View {
let interview: Interview
var body: some View {
HStack {
// Left column: title and subtitle
VStack(alignment: .leading, spacing: 4) {
Text(interview.technology)
.font(.headline)
Text(interview.difficulty)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
// Badge with question count
Text("\(interview.questionCount) Q")
.font(.caption)
.fontWeight(.medium)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
}
.padding(.vertical, 4)
}
}Extraer InterviewRow en su propio struct hace que el codigo sea mas legible y reutilizable. Esta es una buena practica habitual en SwiftUI.
Acciones: Eliminar y Reordenar con iOS 19
Las listas en iOS soportan de forma nativa la eliminacion (deslizar a la izquierda) y el reordenamiento (arrastrar y soltar). SwiftUI simplifica esto con .onDelete y .onMove:
struct InterviewListView: View {
@State private var interviews = [/* ... data ... */]
var body: some View {
NavigationStack {
List {
ForEach(interviews) { interview in
InterviewRow(interview: interview)
}
// Swipe to delete
.onDelete(perform: deleteInterview)
// Drag and drop to reorder
.onMove(perform: moveInterview)
}
.navigationTitle("My Interviews")
.toolbar {
// "Edit" button that activates edit mode
EditButton()
}
}
}
// Deletes elements at specified indices
private func deleteInterview(at offsets: IndexSet) {
interviews.remove(atOffsets: offsets)
}
// Moves elements from one position to another
private func moveInterview(from source: IndexSet, to destination: Int) {
interviews.move(fromOffsets: source, toOffset: destination)
}
}iOS 19 introduce contenedores reordenables: el nuevo modificador .reorderable() combinado con reorderContainer(for:) permite reordenar elementos en List, LazyVGrid y layouts personalizados con animaciones integradas. El sistema calcula automaticamente las diferencias mediante un parametro closure. Esto funciona tambien en watchOS, una novedad para el dispositivo wearable de Apple.
Ademas, las swipe actions ahora funcionan en cualquier vista. Antes limitadas a List, el modificador swipeActions combinado con el nuevo swipeActionsContainer() funciona en ScrollView, lazy stacks y layouts personalizados.
Para mas detalles sobre patrones de navegacion, consultar SwiftUI NavigationStack: preguntas de entrevista.
Animaciones en la Interfaz
SwiftUI sobresale en animaciones. A diferencia de UIKit, donde las animaciones requerian gran cantidad de codigo, aqui todo es declarativo: se describe el estado final y SwiftUI anima la transicion.
Animaciones Implicitas con withAnimation
La forma mas sencilla de animar es envolver un cambio de estado en withAnimation. SwiftUI detecta que cambia y anima automaticamente las propiedades visuales afectadas.
struct AnimatedCard: View {
@State private var isExpanded = false
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
// Dimensions change based on state
.frame(
width: isExpanded ? 300 : 150,
height: isExpanded ? 200 : 100
)
Button(isExpanded ? "Collapse" : "Expand") {
// withAnimation animates ALL visual changes
// resulting from this state change
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
isExpanded.toggle()
}
}
.padding(.top)
}
}
}Al tocar el boton, isExpanded cambia de estado. Gracias a withAnimation, el cambio de tamano del rectangulo se anima con un efecto de resorte. No es necesario especificar que animar.
Transiciones: Animando la Aparicion y Desaparicion
Las transiciones definen como aparece o desaparece una vista. Por defecto es un desvanecimiento (opacity), pero se puede personalizar:
struct TransitionDemo: View {
@State private var showDetails = false
var body: some View {
VStack(spacing: 20) {
Button("Show Details") {
withAnimation(.easeInOut(duration: 0.3)) {
showDetails.toggle()
}
}
// This view appears/disappears with a transition
if showDetails {
DetailCard()
// Asymmetric transition: different for entry and exit
.transition(
.asymmetric(
insertion: .scale.combined(with: .opacity), // Entry: zoom + fade
removal: .slide // Exit: slide
)
)
}
}
}
}
struct DetailCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Interview Details")
.font(.headline)
Text("25 questions • 45 minutes")
.foregroundColor(.secondary)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(16)
}
}En este ejemplo, la tarjeta aparece con zoom desde el centro (scale + opacity) y desaparece deslizandose hacia un lado (slide). Estas microanimaciones hacen que la interfaz se sienta viva y profesional.
SwiftUI optimiza las animaciones automaticamente. Se recomienda preferir .spring() para una sensacion natural y evitar animaciones excesivamente largas (> 0.5s) que frustran a los usuarios.
Carga de Datos Asincronos
En aplicaciones reales, los datos suelen provenir de una API. Swift Concurrency (async/await) se integra de forma fluida con SwiftUI a traves del modificador .task.
El Patron Loading, Error, Success
Este es el patron estandar para mostrar datos de una API. Se manejan tres estados: cargando, error y exito.
struct AsyncDataView: View {
@State private var questions: [Question] = []
@State private var isLoading = true
@State private var errorMessage: String?
var body: some View {
Group {
if isLoading {
// State: loading in progress
ProgressView("Loading...")
} else if let error = errorMessage {
// State: error
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundColor(.orange)
Text(error)
Button("Retry") {
Task { await loadQuestions() }
}
}
} else {
// State: success, display data
List(questions) { question in
Text(question.title)
}
}
}
// .task runs automatically when the view appears
.task {
await loadQuestions()
}
// Pull-to-refresh
.refreshable {
await loadQuestions()
}
}
private func loadQuestions() async {
isLoading = true
errorMessage = nil
do {
// Async API call
questions = try await QuestionService.shared.fetchQuestions()
} catch {
errorMessage = "Unable to load questions"
}
isLoading = false
}
}El modificador .task es esencial: lanza una tarea asincrona cuando la vista aparece y la cancela automaticamente cuando desaparece. No hay posibilidad de fugas de memoria. Para mas detalles sobre patrones async en Swift, consultar Preguntas de entrevista Swift Structured Concurrency.
Fuentes
- What's New in SwiftUI - WWDC 2026 - Vision general de los cambios de SwiftUI en iOS 19
- Documentacion SwiftUI @State - Referencia oficial de la macro @State e inicializacion diferida
- Swift 6.3 Release Notes - Mejoras del lenguaje que soportan las ganancias de rendimiento de SwiftUI
- SwiftUI in WWDC 2026 - Analisis detallado de las nuevas API
Puntos clave de SwiftUI en iOS 19
SwiftUI se ha convertido en la herramienta fundamental para el desarrollo moderno de iOS. Con iOS 19, el framework ha alcanzado una madurez que lo hace perfectamente adecuado para aplicaciones en produccion.
- Paradigma declarativo: se describe lo que se desea, no como construirlo
- @State y @Binding: gestion reactiva del estado y propagacion entre vistas, ahora con inicializacion diferida
- Stacks: combinar VStack, HStack y ZStack para layouts flexibles
- List: mostrar colecciones con codigo minimo e interacciones nativas
- Contenedores reordenables: iOS 19 agrega
.reorderable()para arrastrar y soltar en cualquier contenedor - Swipe actions en cualquier lugar: ya no limitadas a List, funcionan en ScrollView y layouts personalizados
- Animaciones: usar
withAnimationpara transiciones suaves automaticas - Async/await: cargar datos con
.tasky manejar estados de carga y error
Lista de Verificacion para Entrevistas
- Comprender la diferencia entre imperativo (UIKit) y declarativo (SwiftUI)
- Dominar los modificadores y su orden de aplicacion
- Saber cuando usar @State vs @Binding vs @Observable
- Construir layouts con Stacks
- Implementar listas con acciones (eliminar, mover, iOS 19 reorderable)
- Animar cambios de estado con withAnimation
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
SwiftUI abre las puertas a la construccion de aplicaciones elegantes en todo el ecosistema Apple. La mejor forma de aprender es practicando: crear un pequeno proyecto personal y experimentar con cada concepto de este articulo resulta el camino mas efectivo.
¿Sabrías detectar el bug en iOS?
Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Escrito por
Anthony Fillion-MailletFundador de SharpSkill
Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.
Actualizado el 20 de agosto de 2026
Etiquetas
Compartir
Artículos relacionados

Rendimiento SwiftUI: Optimización de LazyVStack y Listas Complejas
Técnicas de optimización para LazyVStack y listas SwiftUI. Reducir el consumo de memoria, mejorar el rendimiento del scroll y evitar errores comunes.

ViewModifiers personalizados en SwiftUI: patrones reutilizables para Design Systems
Construye ViewModifiers personalizados en SwiftUI para un design system coherente. Patrones, mejores prácticas y ejemplos prácticos para estilizar vistas iOS de forma eficiente.

SwiftUI @Observable vs @State: Cuándo Usar Cada Uno en 2026
Domina las diferencias entre @Observable y @State en SwiftUI para elegir la herramienta de gestión de estado adecuada en aplicaciones iOS.