iOS desde cero (I): Persistencia de datos

Introducción

En este artículo se explica de manera muy básica los dos tipos de persistencia de datos que existen en iOS y cómo trabajar con ellos.

Persistencia básica: UserDefaults

El primer tipo de persistencia básica que tenemos son los llamados UserDefaults, que no vienen a ser otra cosa que pares clave-valor que se utilizan normalmente para guardar preferencias del usuario y así personalizar su interacción con la app. UserDefaults no es otra cosa que el equivalente a SharedPreferences en Android.

Para guardar valores en los UserDefaults, llamamos a la siguiente función:

let key = "autoLogin"
let value = true
UserDefaults.standard.set(value, forKey: key)

UserDefaults admite los tipos básicos, además de arrays y diccionarios. Cualquier otro tipo que se quiera guardar debe archivarse como un objeto de tipo NSData.

Por otra parte, para recuperar valores:

let key = "items"
let itemsObj = UserDefaults.standard.value(forKey: key)
if let items = itemsObj as? [String] {
    // items were correctly retrieved and casted, otherwise this won't run
}

Nota: UserDefaults no está pensado para utilizarse como una base de datos, puesto que todo se carga en memoria y es accesible desde cualquier lado.

Base de datos: CoreData

Si queremos algo más avanzado, tendremos que utilizar una base de datos. Para ello, Apple nos ofrece el ORM CoreData, que utiliza una base de datos de SQLite por debajo.

CoreData es el equivalente a Room en Android.

Añadiendo CoreData al proyecto

Para añadir CoreData, cuando creemos un proyecto, tenemos que marcar la casilla que dice "Use CoreData" en XCode. Una vez hecho esto, veremos un archivo de base de datos que podemos abrir y que nos presentará una interfaz como la siguiente:

imagen.png

Mediante esta interfaz podremos crear nuevas entidades, establecer relaciones entre ellas, restricciones (claves únicas), etc.

Operando con la base de datos

Empezar a operar con la base de datos no es difícil, y de hecho tiene menos instalación que si por ejemplo queremos utilizar Room en Android. No obstante, las consultas son un tanto más complicadas.

Antes de realizar cualquier operación con la base de datos necesitamos obtener un contexto. Para ello, basta con escribir las dos siguientes líneas:

let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext

Veamos cómo operar con la base de datos. Para todos los ejemplos vamos a crear una clase en nuestro código llamada Car. Esta clase no es nada de la base de datos, sino que es una clase normal de Swift que estará en nuestro model:

struct Car {
    var make: String
    var model: String
    var year: Int
    let licensePlate: String
}

Insertando nuevas filas

Para insertar una nueva fila tenemos que llamar a la función NSEntityDescription.insertNewObject, que crea un nuevo objeto al que le podemos poner pares clave-valor:

func insert(car: Car) {
    let carDbObject = NSEntityDescription.insertNewObject(forEntityName: "Cars", into: context)
    newCar.setValue(car.licensePlate, forKey: "licensePlate")
    newCar.setValue(car.make, forKey: "make")
    newCar.setValue(car.model, forKey: "model")
    newCar.setValue(car.year, forKey: "year")

    do {
        try context.save()
    } catch {
        // handle errors
    }
}

Obteniendo filas

Para pedir todos los resultados de una tabla a la base de datos, tenemos que crear una NSFetchRequest. Esto nos devolverá un objeto de tipo NSFetchRequestResult que tendremos que castear a NSManagedObject. Veámoslo con un ejemplo:

func getAll() -> [NSManagedObject] {
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Cars")
    request.returnObjectsAsFaults = false // this is important!

    do {
        let results = try context.fetch(request)
        if results.count > 0 {
            return results as! [NSManagedObject]
        } else {
            return []
        }
    } catch {
        return []
    }
}

También podemos filtrar, por ejemplo, podríamos querer obtener un coche en concreto dada su matrícula. El método no cambia demasiado, solo hay que añadir un predicado para filtrar los items:

func get(withLicensePlate licensePlate: String) -> NSManagedObject? {
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Cars")
    request.predicate = NSPredicate(format: "licensePlate = %@", licensePlate)
    request.returnObjectsAsFaults = false // this is important!

    do {
        let results = try context.fetch(request)
        if results.count > 0 {
            return (results.first as! NSManagedObject)
        } else {
            return nil
        }
    } catch {
        return nil
    }
}

Actualizando filas

La operación de actualizar una fila es bastante obvia, puesto que lo único que necesitamos es conseguir nuestro NSManagedObject, modificarlo y guardar los cambios en el context. Para conseguir el objeto podemos utilizar la función de la sección anterior, y la de modificar quedaría tal que así:

func update(item: NSManagedObject, withValues newValues: Car) {
    item.setValue(newValues.make, forKey: "make")
    item.setValue(newValues.model, forKey: "model")
    item.setValue(newValues.year, forKey: "year")

    do {
        try context.save()
    } catch {
        // handle errors
    }
}

Eliminando filas

Por último, podemos querer eliminar filas de nuestra base de datos. Nuevamente, el método es muy fácil puesto que solo necesitamos tener la instancia del NSManagedObject, borrarla y guardar el contexto. Se haría así:

func delete(item: NSManagedObject) {
    context.delete(item)

    do {
        try context.save()
    } catch {
        // handle errors
    }
}

Conclusiones

Con este pequeño tutorial quedan cubiertas las necesidades básicas de cualquier CRUD, que es uno de los puntos básicos para empezar a desarrollar aplicaciones.