Modelar las entidades en código Swift
Desde el punto de vista del código Swift las entidades del modelo serán instancias de clases Swift. Así, dada la entidad Usuario con ciertos atributos lo que tendremos en Swift es una clase con el mismo nombre y las correspondientes propiedades.
Generación automática de las clases¶
Las clases de nuestro modelo de datos se pueden generar automáticamente y de modo transparente para el desarrollador cada vez que guardemos el modelo de datos (el .xcdatamodeld
).
En el editor del modelo de datos, si seleccionamos una entidad y accedemos a sus atributos en el panel de la derecha, veremos que hay una sección titulada Class
que se ocupa del código generado, podemos cambiar:
- El nombre de la clase generada (por defecto el de la entidad)
- El módulo de Swift donde se crea la clase (por defecto pone
Global Namespace
, lo que significa que no hará falta ningúnimport
para referenciar la clase) - La estrategia de generación de código o
codegen
. Aquí por defecto aparece seleccionada la opciónClass definition
, que indica que Xcode va a generar automáticamente todo el código asociado a la entidad. ConManual/None
habría que generar el código explícitamente. ConCategory/Extension
parte del código se generará automáticamente y parte de forma manual.
Las clases se vuelven a generar si ha habido cambios cada vez que guardamos el modelo de datos (File>Save
o Cmd-S
). No se generan en el directorio del proyecto, de modo que no son visibles directamente, sino en un directorio aparte denominado Derived Data
, donde Xcode coloca típicamente el código auxiliar generado por él. Este directorio por defecto está en otro lugar totalmente distinto del proyecto, podemos ver dónde está en la opción File>Project Settings
, aunque no es necesario verlo ni recomendable modificar el contenido generado.
Si queremos "echarle un vistazo" al fuente de las clases generadas sin tener que ir a la carpeta podemos usar el "truco" de hacer
Cmd-Click
sobre el nombre de una clase que tengamos en el código. Xcode abre automáticamente el archivo en el que se define. Esto funciona con las clases de nuestro proyecto, con las autogeneradas y almacenadas enDerived Data
e incluso con clases del sistema, como por ejemploUIViewController
.
Cuando una entidad tiene relaciones a uno o a muchos podemos manipular estas relaciones a través de propiedades y métodos de la clase generada. Veremos esto en la sección dedicada a CRUD.
Generación manual de las clases¶
Desde la versión 8 de Xcode el modo "automático" es el activo por defecto. No obstante en algunos casos nos interesará personalizar el código generado por Xcode añadiendo métodos y/o propiedades, y para esto no podemos usar la generación automática, ya que por un lado el código no es directamente visible en el proyecto y por otro Xcode sobreescribiría los cambios hechos manualmente.
Xcode tiene un asistente que puede crear las clases que representan a las entidades. Teniendo seleccionada una entidad cualquiera en el editor del modelo de datos, elegimos la opción Editor > Create NSManagedObject Subclass...
en el menú de Xcode. Se activará el asistente, que es bastante sencillo de usar. Solo tenemos que elegir el modelo de datos (si es que tenemos más de uno) y las entidades para las que vamos a generar clases.
Por cada entidad Xcode generará dos archivos:
- Una clase con el mismo nombre que la entidad (aunque el nombre se puede cambiar). Esta clase se declara como una subclase de
NSManagedObject
, que es la que proporciona la funcionalidad básica a las entidades - Una extensión Swift de esta clase. En ésta se definen las propiedades de la entidad y un conjunto de métodos que nos facilitarán el trabajo con las relaciones entre entidades.
IMPORTANTE: En este modo de funcionamiento, Xcode no mantiene automáticamente sincronizado el modelo de datos y el código Swift. Si tras generar las clases modificamos las entidades, tendremos que borrar las clases generadas y volver a generarlas
Vamos a ver un ejemplo del tipo de código que genera Xcode. Si tuviéramos el modelo de datos de la figura
Tras generar clases para todas las entidades, acabaremos con 6 archivos fuente nuevos, dos por cada entidad. Para cada entidad, el primero de ellos tiene el mismo nombre que la entidad terminado en +CoreDataClass.swift
. Si lo abrimos veremos que es la definición de una clase "vacía", marcada con algunas anotaciones especiales para que funcione correctamente la maquinaria interna de Core Data. Por ejemplo, para la entidad Usuario
tendríamos algo como:
// Usuario+CoreDataClass.swift
import Foundation
import CoreData
@objc(Usuario)
public class Usuario: NSManagedObject {
}
El segundo de los archivos para cada entidad tiene un nombre terminado en +CoreDataProperties.swift
. Este archivo contiene una extensión de la clase anterior. El código generado tiene dos partes diferenciadas.
En primer lugar tenemos la definición de las propiedades, que en el ejemplo serían algo como
@NSManaged public var login: String?
@NSManaged public var creditos: Int16
@NSManaged public var password: String?
@NSManaged public var mensajes: NSSet?
@NSManaged public var conversaciones: NSOrderedSet?
Nótese que en la definición de la clase tenemos no solo las propiedades de la entidad en sí sino también propiedades que representan las relaciones. Por ejemplo en el diagrama puede verse una relación uno a muchos entre Usuario
y Mensaje
llamada mensajes
, que representaría los mensajes enviados por un usuario. Esta relación se representa en código con la propiedad del mismo nombre. Esto quiere decir que si tenemos un usuario y vamos imprimiendo los objetos contenidos en la propiedad mensajes
en realidad estaremos accediendo a la entidad Mensaje
. Esto es mucho más sencillo y "limpio" que andar haciendo JOINs en SQL para obtener los datos relacionados.
Que las relaciones se vean en código como propiedades es lo que explica la convención de darle siempre a una relación el nombre de la entidad destino en singular si es "a uno" y en plural si es "a muchos", ya que en código queda bastante natural. Por ejemplo dado un usuario
u
sus mensajes serían accesibles comou.mensajes
, y dado un mensajem
la conversación a la que pertenece estaría enm.conversacion
.
Nótese que las relaciones "uno a muchos" se modelan con NSSet
si no son ordenadas. Con esta estructura de datos no se nos garantiza un orden determinado al ir iterando por ella. En el caso de ser una relación marcada como ordenada Xcode habría generado un NSOrderedSet
.
NSSet
es el "equivalente" al tipo Set
de Swift, pero NSSet
está definido en la librería Foundation
y no en la librería estándar de Swift, ya que se desarrolló inicialmente para usarse en Objective C, que no tiene un tipo nativo para definir conjuntos. Hay que tener en cuenta que Core Data se desarrolló originalmente para Objective C, por lo que la infraestructura para implementarlo se ha "heredado" de éste.
NSSet
y NSOrderedSet
complican un poco el trabajo cuando en lugar de simplemente leer datos queremos modificarlos (añadir un nuevo mensaje a una conversación, por ejemplo), ya que en Objective C se diferencia entre colecciones mutables e inmutables. En Swift lo que hace que una colección sea mutable o no es si la declaramos con let
o con var
, como en el resto de tipos, pero en Foundation
cada colección tiene dos variantes, la "versión" mutable y la inmutable. Así , los NSSet
son inmutables, de modo que no podríamos añadir más elementos a la relación usando directamente la variable. Tendríamos que obtener una copia mutable de la colección y trabajar con ella. Una posibilidad para hacer esto es llamar al método mutableSetValueForKey
de NSManagedObject
, que nos devolverá un conjunto mutable para una propiedad determinada.
Para facilitar el trabajo con las relaciones "a muchos" Xcode también genera métodos de acceso para las colecciones, o accesores. Estos accesores nos permiten añadir/eliminar elementos de la colección. En el caso del Usuario
, generará algo como:
// MARK: Generated accessors for mensajes
extension Usuario {
@objc(addMensajesObject:)
@NSManaged public func addToMensajes(_ value: Mensaje)
@objc(removeMensajesObject:)
@NSManaged public func removeFromMensajes(_ value: Mensaje)
@objc(addMensajes:)
@NSManaged public func addToMensajes(_ values: NSSet)
@objc(removeMensajes:)
@NSManaged public func removeFromMensajes(_ values: NSSet)
}
De este modo en nuestro código podemos establecer una relación entre un determinado usuario y un determinado mensaje sin más que llamar al método addToMensajes
. Por ejemplo podríamos hacer algo como:
//omitimos la parte en la que obtenemos una referencia al contexto de Core Data
let u = Usuario(context:miContexto)
u.login = "Pepe"
u.password = "123456"
let m = Mensaje(context:miContexto)
u.addToMensajes(m)
try! miContexto.save()
Generación "semi-automática" de las clases¶
En este modo se generaría automáticamente solo la extensión Swift donde se definen las propiedades y las relaciones, permitiendo que se mantenga una sincronización automática entre los atributos del modelo de datos y el código Swift.
Por otro lado la clase "principal" que hereda de NSManagedObject
debemos generarla manualmente como en el apartado anterior, lo que nos permite añadir código propio a la clase (por ejemplo métodos de lógica de negocio o métodos auxiliares) sin problemas de que Xcode nos sobreescriba el código.
Este modo se elige en el editor del modelo de datos yendo a las propiedades de la entidad y seleccionando la opción Category/Extension
del desplegable Codegen
.