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

SwiftUI ha revolucionado 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 18 incorpora mejoras significativas en rendimiento y nuevas capacidades.
SwiftUI alcanza su madurez con iOS 18, ofreciendo renderizado optimizado, mejor gestion de memoria 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.
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: es automatico.
@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) {
// We 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.
¿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
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)
}
}Con solo 4 lineas adicionales de codigo (.onDelete, .onMove, EditButton y las dos funciones), se obtiene una lista completamente interactiva. Esa es la eficiencia de SwiftUI.
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: SwiftUI lo determina automaticamente.
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.
Conclusion
SwiftUI se ha convertido en la herramienta fundamental para el desarrollo moderno de iOS. Con iOS 18, el framework ha alcanzado una madurez que lo hace perfectamente adecuado para aplicaciones en produccion.
Puntos Clave
- Paradigma declarativo: se describe lo que se desea, no como construirlo
- @State y @Binding: gestion reactiva del estado y propagacion entre vistas
- Stacks: combinar VStack, HStack y ZStack para layouts flexibles
- List: mostrar colecciones con codigo minimo e interacciones nativas
- Animaciones: usar
withAnimationpara transiciones suaves automaticas - Async/await: cargar datos con
.tasky manejar estados de carga y error
Lista de Verificacion
- 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)
- 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.
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.