En el desarrollo de aplicaciones modernas, la experiencia del usuario (UX) lo es todo. Un aspecto fundamental de una buena UX es que la aplicación “recuerde” las preferencias del usuario. Ya sea el modo oscuro, el tamaño de la fuente, o si el usuario ya ha visto la pantalla de bienvenida, esperar que el usuario configure la app cada vez que la abre es un error fatal.
Antes de SwiftUI, persistir estas pequeñas piezas de información requería un baile constante con UserDefaults, sincronizando manualmente la interfaz de usuario con los datos guardados. Con la llegada de SwiftUI, Apple introdujo @AppStorage, un Property Wrapper (envoltorio de propiedad) que simplifica este proceso de manera elegante y reactiva.
En este tutorial exhaustivo, exploraremos qué es @AppStorage, cómo funciona bajo el capó, y cómo implementarlo en aplicaciones reales para iOS, macOS y watchOS.
1. ¿Qué es exactamente @AppStorage?
Para entender @AppStorage, primero debemos recordar a su ancestro: UserDefaults.
UserDefaults es una base de datos clave-valor simple que almacena datos de configuración ligeros (Strings, números, Booleanos) en el sistema de archivos del dispositivo (.plist). Es rápido, eficiente para datos pequeños, pero no es “reactivo”. Si cambias un valor en UserDefaults en el código antiguo, tenías que decirle manualmente a la vista que se actualizara.
@AppStorage es el puente mágico entre UserDefaults y la interfaz declarativa de SwiftUI.
Características Clave:
- Reactividad: Funciona como
@State. Cuando el valor en el disco cambia,@AppStorageinvalida la vista automáticamente y SwiftUI la renderiza de nuevo con el nuevo valor. - Simplicidad: Elimina la necesidad de escribir
UserDefaults.standard.set(...)oUserDefaults.standard.integer(forKey: ...). - Fuente de la Verdad: Se convierte en una “Source of Truth” para configuraciones de usuario que persisten entre lanzamientos de la app.
Nota Importante:
@AppStorageno está diseñado para bases de datos complejas (como una lista de 5,000 tareas). Para eso existen CoreData, SwiftData o Realm.@AppStoragees para preferencias y configuraciones.
2. Sintaxis y Tipos Soportados
La anatomía de una declaración @AppStorage es sencilla, pero potente.
@AppStorage("clave_unica_en_disco") var nombreVariable: Tipo = valorPorDefectoDesglose de componentes:
- La Clave (“key”): Es el string que identifica el valor en el archivo
UserDefaults. Si dos vistas usan la misma clave, compartirán el mismo dato. - La Variable: El nombre que usarás dentro de tu código Swift.
- El Tipo: Debe ser explícito o inferido.
- Valor por Defecto: El valor que tendrá la variable si es la primera vez que se ejecuta la app y no hay nada guardado en el disco.
Tipos de Datos Soportados
De forma nativa, @AppStorage soporta los mismos tipos que UserDefaults:
BoolIntDoubleStringURLData
3. Implementación Práctica: Creando una Pantalla de Configuración
Vamos a construir un ejemplo práctico. Imagina una aplicación de lectura donde el usuario puede configurar:
- Si quiere recibir notificaciones (Bool).
- Su nombre de usuario (String).
- El tamaño de la fuente de lectura (Double).
Paso 1: Configurar las variables
Dentro de tu View en SwiftUI (por ejemplo, SettingsView.swift), declaramos las propiedades:
import SwiftUI
struct SettingsView: View {
// 1. Persistencia de un Booleano
@AppStorage("notificacionesEnabled") private var notificacionesEnabled = true
// 2. Persistencia de un String
@AppStorage("username") private var username = "Invitado"
// 3. Persistencia de un Double
@AppStorage("fontSize") private var fontSize = 14.0
var body: some View {
NavigationView {
Form {
Section(header: Text("Perfil")) {
// El binding ($) escribe automáticamente en UserDefaults
TextField("Nombre de usuario", text: $username)
}
Section(header: Text("Preferencias")) {
Toggle("Recibir Notificaciones", isOn: $notificacionesEnabled)
VStack(alignment: .leading) {
Text("Tamaño de letra: \(Int(fontSize)) pts")
Slider(value: $fontSize, in: 10...30, step: 1)
}
}
Section {
// Botón para resetear (lógica de negocio simple)
Button("Restablecer valores") {
notificacionesEnabled = true
username = "Invitado"
fontSize = 14.0
}
.foregroundColor(.red)
}
}
.navigationTitle("Configuración")
}
}
}¿Qué está pasando aquí?
Cuando el usuario mueve el Slider o escribe en el TextField:
- El valor cambia en la memoria.
- SwiftUI escribe inmediatamente el nuevo valor en
UserDefaultsbajo la clave asignada. - Si cierras la app (la matas desde la multitarea) y la vuelves a abrir, los controles aparecerán exactamente como los dejaste. Cero código extra de carga o guardado.
4. Persistencia Avanzada: Enums y Estructuras
Aquí es donde muchos desarrolladores se atascan. ¿Qué pasa si quiero guardar una opción de un enum o una structcompleja?
Guardando Enumeraciones (Enums)
Para guardar un enum en @AppStorage, el enum debe conformar al protocolo String (o Int) y CaseIterable (opcional, pero útil para Pickers).
enum AppTheme: String, CaseIterable, Identifiable {
case system = "Sistema"
case light = "Claro"
case dark = "Oscuro"
var id: String { self.rawValue }
}
struct AppearanceView: View {
// Swift entiende que debe guardar el 'rawValue' (String) del enum
@AppStorage("appTheme") private var selectedTheme: AppTheme = .system
var body: some View {
Picker("Tema de la App", selection: $selectedTheme) {
ForEach(AppTheme.allCases) { theme in
Text(theme.rawValue).tag(theme)
}
}
}
}Guardando Structs y Objetos Complejos (JSON)
@AppStorage no soporta Structs o Classes directamente. Si intentas hacer @AppStorage("user") var user: UserStruct, el compilador dará error.
Para solucionar esto, necesitamos conformar RawRepresentable y usar JSONEncoder para convertir el objeto en un String o Dataque UserDefaults sí entienda.
Aquí tienes una extensión reutilizable que puedes copiar y pegar en tus proyectos:
import SwiftUI
// Extensión para permitir guardar cualquier objeto Codable en AppStorage
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
// Ejemplo de uso
struct Task: Codable, Identifiable, Hashable {
var id = UUID()
var title: String
var isCompleted: Bool
}
struct TaskListView: View {
// Ahora podemos guardar un Array de tareas directamente
@AppStorage("savedTasks") var tasks: [Task] = []
var body: some View {
List {
ForEach(tasks) { task in
Text(task.title)
}
}
.onAppear {
// Añadimos una tarea de prueba si está vacío
if tasks.isEmpty {
tasks.append(Task(title: "Aprender SwiftUI", isCompleted: false))
}
}
}
}Advertencia de Rendimiento: Codificar y decodificar JSON en cada pulsación de tecla o cambio pequeño puede ser costoso si el objeto es muy grande. Úsalo con moderación.
5. @AppStorage en el Ecosistema Apple (Multiplataforma)
Una de las bellezas de SwiftUI es su portabilidad. @AppStorage funciona idénticamente en iOS, macOS, watchOS y tvOS, pero hay consideraciones de UX específicas.
En macOS: La Ventana de Preferencias
En macOS, el estándar es tener una ventana dedicada a preferencias (Settings).
@main
struct MiAppMac: App {
var body: some Scene {
WindowGroup {
ContentView()
}
// Define la escena de configuración estándar de macOS
Settings {
SettingsView() // La misma vista que usamos en iOS
.frame(width: 400, height: 300)
}
}
}Al usar @AppStorage en SettingsView, cualquier cambio que hagas en esa ventana se propagará instantáneamente a ContentView u otras ventanas abiertas, ya que ambas observan la misma clave en UserDefaults.
En watchOS
En el Apple Watch, las interacciones son cortas. @AppStorage es perfecto para guardar el estado de una sesión de entrenamiento o una preferencia de visualización. Sin embargo, evita guardar grandes cantidades de datos aquí para no impactar la batería.
6. Compartiendo datos entre la App y Widgets (App Groups)
Si estás creando un Widget para la pantalla de inicio (Home Screen), te darás cuenta de que tu Widget no puede leer el @AppStorage de tu app principal por defecto. Esto se debe a que cada ejecutable vive en su propia caja de arena (Sandbox).
Para compartir datos, necesitas usar App Groups.
- Xcode: Ve a “Signing & Capabilities” en tu target principal y en el del Widget.
- Añadir Capability: Agrega “App Groups”.
- Crear Grupo: Crea un identificador, ej:
group.com.miempresa.miapp.
Ahora, debes decirle a @AppStorage que use ese grupo en lugar del estándar.
// Definimos el store compartido
extension UserDefaults {
static let shared = UserDefaults(suiteName: "group.com.miempresa.miapp")
}
struct WidgetView: View {
// Inyectamos el store explícitamente
@AppStorage("dailyGoal", store: UserDefaults.shared) var dailyGoal = 500
var body: some View {
Text("Meta: \(dailyGoal)")
}
}Al especificar el parámetro store, tanto la app principal como el widget leen y escriben en el mismo archivo físico.
7. Errores Comunes y Mejores Prácticas
A pesar de su facilidad de uso, hay trampas en las que es fácil caer.
A. No uses claves mágicas (Magic Strings)
Repetir "username" en cinco vistas diferentes es una receta para el desastre (errores tipográficos).
Solución: Crea una extensión de Strings o una estructura de constantes.
enum AppStorageKeys {
static let username = "username_key"
static let isDarkMode = "dark_mode_key"
}
// Uso
@AppStorage(AppStorageKeys.username) var username = "..."B. Datos Sensibles
NUNCA guardes contraseñas, tokens de API, o información de salud en @AppStorage. UserDefaults no está encriptado de forma segura y es fácil de extraer si alguien tiene acceso al dispositivo desbloqueado.
- Usa:
Keychainpara contraseñas. - Usa:
@AppStoragesolo para preferencias de UI (color, tamaño, opciones).
C. El problema del ciclo de actualización
Si tienes dos vistas diferentes editando la misma @AppStorage al mismo tiempo, SwiftUI maneja bien la sincronización, pero ten cuidado con bucles lógicos (View A cambia el dato -> View B reacciona y cambia el dato -> View A reacciona…).
D. Pruebas Unitarias (Unit Testing)
Testear vistas con @AppStorage puede ser complicado porque el estado persiste entre tests. Consejo: Para tus tests, inyecta un UserDefaults en memoria o limpia el UserDefaults.standard en el método setUp o tearDown de tus tests.
// En tus tests
override func tearDown() {
let domain = Bundle.main.bundleIdentifier!
UserDefaults.standard.removePersistentDomain(forName: domain)
}8. AppStorage vs SwiftData vs CoreData
Para cerrar, es vital saber cuándo dejar de usar @AppStorage.
| Característica | @AppStorage (UserDefaults) | SwiftData / CoreData |
| Volumen de datos | Pequeño (< 1MB recomendado) | Grande (Gigabytes) |
| Estructura | Plana (Clave-Valor) | Relacional (Grafos, Tablas) |
| Complejidad | Muy Baja | Media / Alta |
| Velocidad | Instantánea (síncrona) | Asíncrona / Optimizada |
| Consultas | No (solo leer por clave) | Sí (filtros, ordenamiento) |
| Uso Ideal | Ajustes, Flags (“Visto por primera vez”) | Listas de usuarios, Inventarios, Cache offline |
Conclusión
@AppStorage es una de esas herramientas en SwiftUI que te hace preguntarte cómo pudimos vivir sin ella. Reduce el código repetitivo masivamente y se integra perfectamente con la filosofía reactiva del framework.
Al dominar @AppStorage, no solo estás guardando datos; estás creando una experiencia de usuario fluida y personalizada que hace que tu aplicación se sienta profesional y atenta a las necesidades del usuario en iOS, macOS y watchOS.
Recuerda siempre: úsalo para preferencias, usa Keychain para secretos, y SwiftData para tu base de datos principal. Con ese equilibrio, tu arquitectura de datos será sólida como una roca.
Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram










