tutoriales.com

Maestría en SwiftData: Persistencia de Datos de Próxima Generación en iOS con SwiftUI

Este tutorial te guiará a través de SwiftData, el framework de persistencia de datos de próxima generación de Apple. Descubre cómo integrar SwiftData en tus aplicaciones SwiftUI, desde la definición de modelos hasta la gestión de relaciones y la migración de esquemas, facilitando un desarrollo de apps robusto y eficiente.

Intermedio20 min de lectura9 views
Reportar error

🚀 Introducción a SwiftData: El Futuro de la Persistencia en iOS

El mundo del desarrollo iOS está en constante evolución, y Apple nos sorprende con herramientas que buscan simplificar y potenciar la creación de nuestras aplicaciones. Con la llegada de SwiftData en WWDC23, la persistencia de datos ha recibido una renovación significativa. SwiftData es el nuevo framework declarativo para la persistencia de datos, construido sobre Core Data pero diseñado para ser más sencillo, más Swifty y más integrado con SwiftUI.

Si has trabajado con Core Data, sabes que puede ser potente pero también intrincado. SwiftData promete una experiencia de desarrollo mucho más fluida, permitiéndonos definir nuestros modelos de datos utilizando clases de Swift estándar y atributos ligeros. En este tutorial, nos sumergiremos en SwiftData, explorando cómo implementarlo en nuestras aplicaciones SwiftUI para una gestión de datos eficiente y elegante.

💡 Consejo: SwiftData es ideal para proyectos nuevos o para refactorizar proyectos existentes que usen Core Data, especialmente si ya estás utilizando SwiftUI.

🎯 ¿Por Qué SwiftData?

Antes de sumergirnos en el código, es importante entender por qué Apple introdujo SwiftData y cuáles son sus ventajas clave:

  • Integración con Swift y SwiftUI: Diseñado desde cero para ser un framework declarativo que se siente nativo de Swift y se integra a la perfección con SwiftUI, reduciendo el código boilerplate.
  • Menos Código: Elimina gran parte del código de configuración que era necesario en Core Data. Los modelos son clases Swift regulares, no NSManagedObject subclases.
  • Consultas Declarativas: Las consultas son más fáciles de escribir y leer, utilizando predicados Swift y la macro @Query en SwiftUI.
  • Migración Simplificada: Ofrece un enfoque más directo para manejar las migraciones de esquemas.
  • Rendimiento: Al estar construido sobre Core Data, hereda su rendimiento y optimizaciones, pero con una capa de abstracción más amigable.

SwiftData vs. Core Data: Una Comparativa Rápida

CaracterísticaCore DataSwiftData
---------
Modelado de DatosArchivo .xcdatamodeld (visual) y NSManagedObjectClases Swift estándar con @Model macro
APIBasada en Objective-C, verbosaBasada en Swift, concisa, declarativa
---------
Integración SwiftUIRequiere adaptadores, más manualIntegración nativa con @Query, ModelContext, etc.
ConfiguraciónNSPersistentContainer, NSManagedObjectContextModelContainer, ModelContext (más sencillo)
---------
ConsultasNSFetchRequest, NSPredicatePredicados Swift, @Query macro
RelacionesDefinidas en modelo visual y códigoDefinidas con @Relationship en clases Swift
---------
MigraciónNSEntityMigrationPolicy, NSMappingModel@SchemaMigrationPlan, más directa
📌 Nota: SwiftData no reemplaza a Core Data, sino que lo encapsula y lo hace más accesible. Puedes pensar en SwiftData como una capa de abstracción y mejora sobre Core Data.

🛠️ Configurando tu Proyecto SwiftData

Empezar con SwiftData es sorprendentemente simple. Vamos a crear un nuevo proyecto de SwiftUI y configurarlo para usar SwiftData.

Paso 1: Crear un Nuevo Proyecto SwiftUI

Abre Xcode y crea un nuevo proyecto: File > New > Project... Selecciona iOS > App. Asegúrate de que el idioma sea Swift y la interfaz sea SwiftUI. Cuando te pida guardar, verás una casilla de verificación para Use SwiftData. ¡Marca esa casilla! Xcode configurará automáticamente tu proyecto con todo lo necesario.

Inicio File > New > Project iOS > App Seleccionar "Swift" y "SwiftUI" Marcar "Use SwiftData" Final

Si olvidas marcar la casilla o quieres añadir SwiftData a un proyecto existente, no hay problema. Sigue leyendo.

Paso 2: Configuración Manual (si es necesario)

Si no marcaste la casilla o estás añadiendo SwiftData a un proyecto existente, necesitas realizar unos pocos pasos:

  1. Importar SwiftData: Asegúrate de importar el framework en tus archivos donde vayas a usarlo.
import SwiftData
  1. Configurar ModelContainer en tu App: En el archivo principal de tu aplicación (por ejemplo, YourAppNameApp.swift), envuelve tu vista raíz con .modelContainer().
import SwiftUI
import SwiftData

@main
struct YourAppNameApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Item.self]) // Aquí especificas tus modelos
}
}
Aquí, `Item.self` es un placeholder para la primera clase de modelo que crearemos. Puedes pasar un array de todos tus tipos de modelo al contenedor.
🔥 Importante: El `modelContainer` inicializa la base de datos de SwiftData y la hace disponible en el entorno de tu aplicación. Si no lo haces, SwiftData no funcionará.

📖 Definiendo tus Modelos de Datos con @Model

La magia de SwiftData comienza con la macro @Model. Esta macro convierte una clase Swift regular en un tipo de modelo que puede ser persistido y gestionado por SwiftData.

Vamos a crear un modelo simple para una lista de tareas (Task).

import Foundation
import SwiftData

@Model
final class Task {
    var name: String
    var isCompleted: Bool
    var createdAt: Date

    init(name: String, isCompleted: Bool = false, createdAt: Date = Date()) {
        self.name = name
        self.isCompleted = isCompleted
        self.createdAt = createdAt
    }
}

¡Así de sencillo! No necesitas heredar de NSManagedObject, ni implementar CodingKeys, ni preocuparte por NSEntityDescription. La macro @Model se encarga de todo esto bajo el capó.

Atributos y Propiedades Persistibles

Por defecto, todas las propiedades var que sean tipos compatibles con Codable (como String, Int, Double, Bool, Date, UUID, Data, URL y Codable opcionales) se persistirán automáticamente. También se admiten colecciones de estos tipos (Arrays, Sets, Dictionaries).

Si quieres excluir una propiedad de la persistencia, usa @Transient:

@Model
final class Task {
    // ... otras propiedades
    @Transient var someTemporaryValue: String = "" // Esta propiedad no se persistirá
}

💾 Realizando Operaciones CRUD: Crear, Leer, Actualizar y Borrar

Con nuestros modelos definidos, el siguiente paso es interactuar con ellos: guardar nuevas tareas, leer las existentes, modificarlas y eliminarlas.

El ModelContext

ModelContext es el equivalente de NSManagedObjectContext en Core Data. Es tu interfaz principal para interactuar con los modelos persistidos. Lo obtienes del entorno de SwiftUI usando @Environment.

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    // ... tu vista
}

Crear (Create) ✨

Para crear un nuevo objeto, simplemente inicializa una instancia de tu clase de modelo y luego insértala en el modelContext.

func addTask(name: String) {
    let newTask = Task(name: name)
    modelContext.insert(newTask)
    // No es necesario llamar a save() explícitamente en la mayoría de los casos de SwiftUI,
    // ya que el entorno gestiona el guardado automáticamente.
}

Leer (Read) 📚

SwiftData ofrece una forma muy elegante de leer datos utilizando la macro @Query. Esta macro automáticamente recupera y mantiene actualizados los datos de tu vista.

struct TaskListView: View {
    @Query(sort: \.createdAt, order: .reverse) var tasks: [Task]

    var body: some View {
        NavigationView {
            List {
                ForEach(tasks) { task in
                    Text(task.name)
                }
            }
            .navigationTitle("Mis Tareas")
            .toolbar {
                Button("Añadir") {
                    // Lógica para añadir tarea
                }
            }
        }
    }
}

La macro @Query se actualiza automáticamente cuando los datos cambian, lo que simplifica enormemente la creación de interfaces de usuario reactivas.

Predicados y Filtrado

Puedes añadir predicados a tu @Query para filtrar los resultados, similar a NSPredicate pero con sintaxis Swift.

@Query(filter: #Predicate<Task> { $0.isCompleted == false }, sort: \.createdAt, order: .reverse)
var incompleteTasks: [Task]

@Query(filter: #Predicate<Task> { $0.name.contains("comprar") })
var shoppingTasks: [Task]

La macro #Predicate es una característica de Swift 5.9 que ofrece una forma de definir condiciones de filtrado de manera segura y eficiente.

Actualizar (Update) ✏️

Para actualizar un objeto, simplemente modifica sus propiedades. Como los objetos Task son clases (tipos de referencia), los cambios se reflejan automáticamente en el modelContext y se persisten.

func toggleCompletion(for task: Task) {
    task.isCompleted.toggle()
    // No se necesita llamar a modelContext.save() explícitamente aquí tampoco.
}

Borrar (Delete) 🗑️

Para eliminar un objeto, llama al método delete() en modelContext.

func deleteTask(task: Task) {
    modelContext.delete(task)
}

// En una vista SwiftUI con ForEach y onDelete
struct TaskListView: View {
    @Environment(\.modelContext) private var modelContext
    @Query var tasks: [Task]

    var body: some View {
        List {
            ForEach(tasks) { task in
                Text(task.name)
            }
            .onDelete { indexSet in
                for index in indexSet {
                    modelContext.delete(tasks[index])
                }
            }
        }
    }
}

🔗 Gestión de Relaciones en SwiftData

Las relaciones entre modelos son fundamentales para bases de datos relacionales. SwiftData simplifica la gestión de relaciones uno-a-muchos, muchos-a-uno y muchos-a-muchos.

Consideremos un modelo Project que tiene muchas Tasks.

@Model
final class Project {
    var name: String
    @Relationship(deleteRule: .cascade, inverse: \Task.project) var tasks: [Task]

    init(name: String, tasks: [Task] = []) {
        self.name = name
        self.tasks = tasks
    }
}

@Model
final class Task {
    var name: String
    var isCompleted: Bool
    var createdAt: Date
    var project: Project? // Relación inversa

    init(name: String, isCompleted: Bool = false, createdAt: Date = Date(), project: Project? = nil) {
        self.name = name
        self.isCompleted = isCompleted
        self.createdAt = createdAt
        self.project = project
    }
}

Aquí, @Relationship define cómo se maneja la relación:

  • deleteRule: .cascade: Si se elimina un Project, todas sus Tasks asociadas también se eliminarán.
  • inverse: \Task.project: Establece la relación inversa en el modelo Task, apuntando a su propiedad project. Esto es crucial para la integridad referencial y para que SwiftData maneje las relaciones de forma eficiente.
Project name: string 1 N contiene Task name: string isCompleted: bool createdAt: date project: id

Creando y Asignando Relaciones

Cuando creas un Task, puedes asignarle un Project:

func createProjectAndTasks() {
    let newProject = Project(name: "Desarrollo App")
    modelContext.insert(newProject)

    let task1 = Task(name: "Diseñar UI", project: newProject)
    let task2 = Task(name: "Implementar Login", project: newProject)
    modelContext.insert(task1)
    modelContext.insert(task2)

    // Alternativamente, puedes añadir tareas a la propiedad 'tasks' del proyecto
    // newProject.tasks.append(task1)
    // newProject.tasks.append(task2)
    // Esto también funciona y es equivalente.
}

🔄 Migración de Esquemas en SwiftData

A medida que tu aplicación evoluciona, es probable que necesites cambiar tus modelos de datos: añadir nuevas propiedades, eliminar antiguas, cambiar tipos, etc. SwiftData ofrece un mecanismo de migración de esquemas que, aunque basado en Core Data, se presenta de forma más amigable.

Migración Ligera Automática

Para cambios simples (añadir un nuevo atributo opcional, añadir una nueva entidad), SwiftData intentará realizar una migración ligera automáticamente. No necesitas hacer nada especial.

Migraciones Manuales con SchemaMigrationPlan

Para cambios más complejos que la migración ligera no puede manejar (cambios de nombre de propiedades, fusión de entidades, transformaciones de datos), necesitas definir un SchemaMigrationPlan.

Supongamos que queremos añadir una propiedad priority a Task y cambiar el nombre de name a title.

  1. Define las versiones de tu esquema: Crea un nuevo Schema para cada versión importante.
import SwiftData

enum TodoSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self, Project.self]
}

@Model
final class Task {
var name: String // V1 tiene 'name'
var isCompleted: Bool
var createdAt: Date
var project: Project?

init(name: String, isCompleted: Bool = false, createdAt: Date = Date(), project: Project? = nil) {
self.name = name
self.isCompleted = isCompleted
self.createdAt = createdAt
self.project = project
}
}

@Model
final class Project {
var name: String
@Relationship(deleteRule: .cascade, inverse: \Task.project) var tasks: [Task]

init(name: String, tasks: [Task] = []) {
self.name = name
self.tasks = tasks
}
}
}

enum TodoSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self, Project.self]
}

@Model
final class Task {
var title: String // V2 cambia a 'title'
var isCompleted: Bool
var createdAt: Date
var priority: Int // V2 añade 'priority'
var project: Project?

init(title: String, isCompleted: Bool = false, createdAt: Date = Date(), priority: Int = 0, project: Project? = nil) {
self.title = title
self.isCompleted = isCompleted
self.createdAt = createdAt
self.priority = priority
self.project = project
}
}

@Model
final class Project {
var name: String
@Relationship(deleteRule: .cascade, inverse: \Task.project) var tasks: [Task]

init(name: String, tasks: [Task] = []) {
self.name = name
self.tasks = tasks
}
}
}

// El esquema actual de tu app se define fuera de los esquemas versionados
typealias TodoSchema = TodoSchemaV2
  1. Define el Plan de Migración: Crea una clase que conforme a SchemaMigrationPlan.
import SwiftData

enum TodoMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[TodoSchemaV1.self, TodoSchemaV2.self]
}

static var stages: [MigrationStage] {
[.migrating(from: TodoSchemaV1.self, to: TodoSchemaV2.self) {
// Renombrar la propiedad 'name' a 'title' en Task
// Añadir una nueva propiedad 'priority' a Task
// Puedes añadir código para transformar datos si es necesario

// Para el renombrado, SwiftData generalmente lo maneja si se define un mapping
// en el archivo de modelo (similar a Core Data) o usando un transformador.
// Para la migración manual, se puede acceder a los contenedores de origen y destino.
// Aquí, para este ejemplo, asumiremos que SwiftData puede inferir el renombrado
// si las entidades tienen el mismo nombre y solo una propiedad ha cambiado de nombre.

// Para añadir una nueva propiedad con un valor por defecto, no se necesita acción explícita aquí.
// SwiftData lo gestiona automáticamente estableciendo el valor por defecto.
}]
}
}
En este punto, la migración para renombrar propiedades puede ser un poco más manual o requiere la ayuda de un `PropertyMapping`. Para un cambio de nombre puro como este, a menudo se usa una técnica de `renamingIdentifier` en Core Data, y SwiftData puede inferirlo. Sin embargo, para transformaciones de datos más complejas, se escribiría código explícito dentro del bloque `migrating` para iterar sobre las entidades y modificar sus valores.

3. Usar el Plan de Migración en modelContainer:

import SwiftUI
import SwiftData

@main
struct YourAppNameApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: TodoSchema.models, migrationPlan: TodoMigrationPlan.self)
}
}
⚠️ Advertencia: Las migraciones de datos son un tema complejo. Siempre haz copias de seguridad de tus datos antes de probar migraciones en producción. Prueba exhaustivamente tus planes de migración.

📈 Optimizaciones y Buenas Prácticas

Aunque SwiftData simplifica mucho la persistencia, seguir algunas buenas prácticas te ayudará a construir aplicaciones más robustas y eficientes:

  • Definir Relaciones Inversas: Siempre que sea posible, define relaciones inversas (inverse: \Task.project). Esto ayuda a SwiftData a mantener la integridad de los datos y optimizar las consultas.
  • Evitar Cargas Excesivas: @Query es eficiente, pero sé consciente de la cantidad de datos que intentas mostrar de una vez. Usa limit y offset si necesitas paginación, o filtros adecuados.
  • @Transient para Propiedades No Persistibles: Usa @Transient para cualquier propiedad que no necesites guardar en la base de datos, como propiedades computadas o datos temporales de UI.
  • Manejo de Errores: Aunque SwiftData es robusto, los errores pueden ocurrir. Considera envolver operaciones críticas en bloques do-catch si interactúas con el ModelContext fuera de las vistas SwiftUI o en entornos asíncronos.
  • Pruebas Unitarias: Es crucial escribir pruebas unitarias para tus modelos y para las operaciones de CRUD. Puedes configurar un ModelContainer en memoria para tus pruebas.
// Ejemplo de un modelContainer en memoria para pruebas
let config = ModelConfiguration(is           StoredInMemoryOnly: true)
let container = try! ModelContainer(for: [Task.self, Project.self], configurations: config)

// Luego, usa este contenedor para crear un modelContext para tus pruebas
let modelContext = ModelContext(container)
🔥 Importante: Las operaciones de SwiftData en el contexto de SwiftUI suelen ser implícitas. Sin embargo, si necesitas realizar operaciones fuera del ciclo de vida de una vista (ej. en un `Task` en segundo plano), obtén un `ModelContext` a través del `ModelContainer` o pasándolo explícitamente.

🏁 Conclusión: El Potencial de SwiftData

SwiftData representa un gran paso adelante en la persistencia de datos para el ecosistema de Apple. Su enfoque declarativo, su integración nativa con Swift y SwiftUI, y su reducción de la complejidad lo convierten en una herramienta extremadamente atractiva para desarrolladores que buscan construir aplicaciones iOS modernas y eficientes.

Al dominar SwiftData, no solo estarás utilizando lo último en tecnología de Apple, sino que también mejorarás significativamente la mantenibilidad y escalabilidad de tus proyectos. Desde la configuración básica de modelos hasta la gestión avanzada de relaciones y migraciones, SwiftData te empodera para enfocarte más en la lógica de tu negocio y menos en la complejidad de la persistencia.

¡Anímate a integrarlo en tus próximos proyectos y descubre la facilidad con la que puedes gestionar tus datos!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!