¡Atención! Este sitio utiliza cookies.
Si no cambias la configuración de tu navegador, aceptas su uso.

Semillas de Cacao: Core Data III – Acceso a datos

En este artículo vamos a aprender a crear nuevos registros, acceder a ellos, mostrarlos en pantalla, cambiarlos y guardarlos una vez tengamos el modelo de la aplicación creado y CoreDataStack funcionando.

Si tienes algún problema con él, puedes descargarlo desde aquí.

Si te perdiste los anteriores…

  1. Introducción
  2. Modelo
  3. Acceso a datos

De AppDelegate al primer controlador

En este punto deberíamos tener:

  • Las clases del modelo.
  • Un CoreDataStack instanciado en AppDelegate
  • Un ViewController con una propiedad NSManagedObjectContext *context y un inicializador con el que le pasamos dicha propiedad.

El contexto será el que maneje el cotarro, ya que contiene en memoria una referencia a todos los objetos de nuestra base de datos (previa búsqueda) y es quien se encarga de dar la orden de guardar. Es algo así como una bandeja en la que vamos consultando el modelo y alterándolo según va y vuelve. Todos los objetos que carguemos desde el contexto conocen el contexto, su NSManagedObjectContext. Esto es especialmente útil por si no queremos pasar el contexto por todos los controladores de la app, ya que podemos inicializar un controlador con un simple  NSManagedObject (una instancia de la tabla/entidad) y usarlo para recuperar el contexto y guardar. Es decir, aunque nos hace falta el ManagedObjectContext para guardar, no es necesario arrastrarlo por todas las clases para hacerlo, porque todos los objetos que contiene tienen una referencia a él.

Inyección de Dependencia

Inyección de Dependencia

Te dejo un código de muestra con la implementación completa del AppDelegate. Es totalmente trivial.

Si ya tienes tu controlador listo para recibir el contexto desde AppDelegate estamos capacitados para la siguiente parte, así que vamos a nuestro CharactersTVC.

Core Data Table View Controller

Utilizar CoreData con las TableViews es bastante sencillo y directo. Parece que CoreData se hubiera diseñado para ello, pero para hacernos la vida aún más fácil Paul Hegarty, un profesor de la universidad de Stanford (muy bueno por cierto), creó una subclase de UITableViewController llamada CoreDataTableViewController.

Descárgala aquí y cópiala en tu proyecto.

¿Qué hace de especial esta clase? Principalmente, se encargará de recoger la búsqueda, sincronizarla con la tabla (Datasource) y de mantenerla sincronizada con el contexto si lo cambiamos. Sin tener que preocuparnos de cuándo ocurra. Con ella utilizaremos un objeto que no hemos visto hasta ahora y que funciona junto a las NSFetch que habíamos visto: el NSFetchedResultsController.

Una vez esté copiada no te olvides de importarla a tu cabecera dentro del .h y cambiar la clase de la que heredamos. Nos vino bien heredar de UITableviewController en un principio para que Xcode nos generase la plantilla, pero vamos a cascarle esta clase nueva, que es mucho más potente..

¿Listo? Ahora solo nos queda rellenar la tabla, aunque tengamos la base de datos vacía… pero a eso le pondremos remedio pronto…

Rellenando la tabla

Creamos un método al que llamaremos loadCharacters: y lo implementaremos con el siguiente código:

Vamos a explicar qué es lo que ocurre ahí dentro.

  1. Creamos un NSFetch, que es un objeto de búsquedas.
  2. Definimos  qué entidad (la tabla) queremos recuperar (‘Character’ en este caso) y le pasamos el contexto, que ya lo deberíamos tener en una propiedad dentro de nuestra clase controlador.
  3. Para especificarle el orden en el que queremos mostrar los datos en la tabla, creamos un NSSortDescriptor con el campo ‘nombre‘ con orden ascendente (esto es un ORDER BY en toda regla).
  4. Finalmente creamos el NSFetchedResultsController que va a guardar todos los registros que encuentre con el NSFetch. También es necesario que le pasemos el contexto. Dejaremos de momento los 2 últimos campos a nil.
  5. Asignamos el nuevo fetchedResultsController a nuestra propiedad del mismo nombre (la heredamos de CoreDataTableViewController) .

Pues esto ya está. Solo queda hacer lo de siempre. Vamos a contestar a las preguntas del DataSource de la tabla, o mejor dicho, la pregunta, porque lo único que queda por contestar es qué celda devolver. Del resto se ocupa CoreDataTableViewController.

Recuerda importar Character.h para poder recuperarlo del fetchedResultsController y pintarlo en la celda.

 Cargamos la app…

Sería recomendable ejecutar la app ahora para comprobar que efectivamente no tenemos registros en la tabla, pero sobre todo que CoreData no está chillando por ninguna parte. Si encuentras algún problema en este punto házmelo saber en los comentarios. Eso sí, lee bien los errores que te da el compilador, ya que en muchas ocasiones una buena comprensión de la teoría vista hasta ahora puede sacarte del apuro.

¿Funciona? Perfecto. Que sí, que no hay nada que mostrar…, pero para eso estamos.

Creando un nuevo registro.

Creamos un nuevo UIViewController que llamaremos NewCharacterVC con un xib y lo dejaremos como en la imagen.

NewCharacterVC.xib

NewCharacterVC.xib

A continuación implementamos las propiedades y métodos básicos, como de costumbre, así como una propiedad e inicializador con el ManagedObjectContext. Cuando creamos un objeto nuevo necesitamos el contexto necesariamente, ya que como veremos ahora, primero lo creamos a partir de su definición del MOM y después le pedimos al contexto que la inserte.

Ya solo nos queda guardar el nuevo elemento a partir de los datos que haya en las cajas de texto. Vamos a implementar saveCharacter:.

Recuerda antes importar la clase Character.h que generamos anteriormente para poder crear una instancia de la misma.

Nota: no se está contemplando que el usuario pulse en ‘Done’ sin haber introducido datos, pero sería necesario hacerlo.

¡Listo! El método saveCharacter: crea una instancia de ‘Character‘ a partir del EntityDescription (la propia definición de la tabla Character) dentro del contexto que estamos manejando. Después simplemente asignamos las distintas variables desde los valores de los campos de texto de la interfaz, y por último, le pedimos al contexto que añada nuestro personaje a sus registros. Nuestro personaje nuevo ya está en memoria listo para guardarse al disco.

Es muy importante que tengas en cuenta que el nuevo objeto no está todavía en la base de datos. Simplemente se ha guardado en memoria para su posterior inserción, cuando se lo pidamos.

¿Y a qué estamos esperando?

Volviendo a la tabla

Ya tenemos la vista que añade a un personaje. Nos toca hacerla accesible desde la tabla.

Antes de seguir tenemos que:

  • Colocar un botón en la barra de navegación.
  • Pasarle el contexto al nuevo controlador.

Copia el código siguiente al final de CharactersTVC. Es totalmente trivial y no está ligado con CoreData, así que no nos detendremos en él.

Prueba a ejecutar. Deberías ver cómo se van guardando valores en la tabla como por arte de magia. ¡¿Chulo, eh?!

Guardando los datos en disco

Si cierras la app (ya sea interrumpiendo la ejecución desde Xcode o volviendo a la pantalla de inicio del simulador y cerrándola) comprobarás que al volver tus datos no están. No es que se hayan borrado, es que no los llegamos a guardar. La instrucción debe darse de forma explícita. CoreData no va a hacerlo por nosotros.

Volvamos a AppDelegate.m y vamos a implementar los siguientes métodos:

  • applicationWillResignActive:
  • applicationDidEnterBackground:
  • applicationWillTerminate:

En cada uno de ellos vamos a escribir una simple línea de código:

 ¿Ya está?

Sí, ya está. Esto es gracias a que el método ya venía incluido dentro del CoreDataStack. Si quisieras hacer tu propia implementación tendrías que llamar directamente a la propiedad managedObjectContext del CoreDataStack y añadir alguna línea de código más .

 

 Algunas dudas frecuentes

¿Qué debo hacer para pasar de un controlador a otro si lo que quiero es mostrar datos, hacer cambios y luego guardarlos?

En este pequeño tutorial hemos cargado una tabla desde el contexto y añadido un nuevo elemento al mismo. No hemos cargado valores en pantalla de una entidad/tabla (más vale que te quedes con lo de ‘Entidad’) concreta, sino que hemos creado una de cero. Cuando toquemos en una celda para que nos abra un nuevo controlador en el que mostrar todos los datos de un personaje ya no será necesario pasarle el contexto entero, sino algo más específico.

Para ello, cuando creemos un nuevo controlador para saltar a la siguiente pantalla deberemos recuperar el objeto (en este caso el Character) del fetchedResultsController a partir del índice de la tabla que se haya seleccionado y asignarlo al siguiente controlador, que como habrás adivinado no tendrá una propiedad «NSManagedObjectContext *context» sino «Character *someCharacter» (cuidado con la palabra ‘character’ porque es ambigua de cara al lenguaje).

Cuando necesites hacer cambios no tendrás que preocuparte por el contexto. Simplemente asigna nuevos valores al objeto. En caso de necesitar el contexto, recuerda que Character es un ManagedObject, y todos ellos tienen una propiedad managedObjectContext, que efectivamente es el contexto en el cual se está gestionando.

Quiero añadir/asignar a un ManagedObject un valor que resulta ser otro ManagedObject (a un Character un Scenario, su homeLand, por ejemplo) ¿Cómo lo hago?

Fíjate en las clases autogeneradas que creamos anteriormente de NSManagedObject (deberían estar en la carpeta ‘Model’). Deberían tener métodos para añadir objetos externos gracias a las relaciones que creaste en el MOM. Si no encuentras la que buscas, repasa el MOM en el archivo ‘.xcdatamodeld’, y si encuentras algún error, cámbialo y vuelve a generar las clases con el procedimiento ya visto (‘Create NSManagedObjectSublass’).

He cambiado el archivo xcdatamodeld, he vuelto a generar las subclases de NSManagedObject y al ejecutar peta como si no hubiera un mañana. ¿Qué mierda es esta?

Al cambiar datos del MOM, la base de datos cambia y CoreData ya no reconoce la que tienes ahora. Solución rápida: borra la app del Simulador o del teléfono y vuelve a ejecutar. Solución un-poco-coñazo: ve a la carpeta del Simulador del iPhone (si lo estás haciendo así), dentro de la Librería del sistema, encuentra la aplicación (es bastante entretenido, especialmente si eres especialmente prolífico) y borra la base de datos a mano.

Imagen de la máscara de la película SAW "¿Quieres jugar a un juego?"

Busca, busca…

La ruta del simulador del iPhone es: Users/<tu-nombre-de-usuario>/Library/ApplicationSupport/iPhoneSimulator.  Tendrás que abrir la carpeta de la versión del OS correspondiente.

Creo que he perpetrado el código por encima de mis posibilidades.

Aquí tienes un enlace del que te puedes descargar todo el proceso que hemos seguido.

CoreDataTutorial

Esto no guarda.

Recuerda que solo se escribe en disco cuando la app pasa a segundo plano o cuando la eliminamos desde el gestor de multitarea. Si estás cerrando la aplicación desde Xcode sin haber hecho alguna de las anteriores, no se guardará nada. El método ‘applicationWillTerminate:’ no se ejecuta si apuñalas a la app en tiempo de ejecución. Mátala, pero avisa antes.

Eso es todo.

¡Nos vemos en otro artículo! No dudes en exponer tus dudas o lamentos en los comentarios.

Escrito por Miguel Hernández Jaso

Autor del blog. Desarrollador especializado en iOS.

7 respuestas a “Semillas de Cacao: Core Data III – Acceso a datos”

    • ¡Espero que te haya servido de ayuda! 🙂
      Es un framework muy potente, pero tiene una curva de aprendizaje inicial muy empinada…

  1. Hola Miguel, enhorabuena por el tutorial, me ha ayudado a entender un poco mejor CoreData.

    Está bien tener la clase CoreDataStack separada, pero creo que con el tick de CoreData activado al crear el proyecto y dejar así que se autogenere la plantilla en el AppDelegate no hay problema, creando un clase de ayuda que recupere el managedObjectContext del AppDelegate.

    Por otra parte, ¿porque esperar a cerrar la app para guardar?¿porque no guardar en el método saveCharacter?

    un saludo!

    • Hola Edu,

      En efecto, podrías dejar el check marcado todo funcionaría exactamente igual. El problema es que dentro de AppDelegate solo debería estar aquello imprescindible y necesario, instrucciones que solo podrían pertenecer a los eventos que se invocan en esta instancia. Es una clase que ya de por sí asume demasiada responsabilidad (es un «massive controller» de la app), así que, en mi opinión, cuanto más se extraiga de ella, mejor.

      A lo que comentas sobre el guardado, es cierto que existe la posibilidad de guardar justo tras ‘saveCharacter:’. La primera razón por la que no lo he hecho así ha sido por fines didácticos, para demostrar que el guardado en memoria y el guardado en disco son pasos distintos, y que pueden estar completamente separados en el código y en el tiempo. Por otra parte, considerando que estamos hablando de una app móvil, lo habitual es posponer este tipo de acciones por temas de rendimiento; es decir, pudiendo guardar 5 personajes de una sola vez, ¿por qué guardar cada uno por separado? Claramente, esto es un detalle de implementación. Podría haberlo hecho con un temporizador utilizando GDC, posponiendo el guardado —si hay cambios— cada 30 segundos. Tema de gustos y sobre todo, de cada app (yo no soy de la opinión de decirle a nadie cómo tiene que hacer su trabajo).

      Son buenas preguntas. Mi respuesta, como ves, es que cada caso es particular, y que este ejemplo está ideado para comprender el funcionamiento de CoreData. Ni está debidamente refactorizado, ni responde a la implementación real de una app en producción.

      Gracias por comentar. Ayuda mucho a seguir trabajando en el blog. Y me alegra saber que he podido ayudarte en entender este framework tan ‘monstruoso’

      ¡Un saludo!

  2. Hola Miguel!!
    Primero felicitarte por tu blog porque, hablando mal y pronto, es la ostia!!!

    Tengo una duda de un proyecto que estoy haciendo y ando buscando info (así he llegado a tu blog) y digo a ver si me puede ayudar. El proyecto inicialmente trabaja sin bbdd y va todo por conexiones. Ahora el cliente dice que quiere que ciertas partes puedan ser descargadas al dispositivo para que funcionen de manera offline. Mi primer reside en que claro, ya está todo montado con sus NSObjects y funcionando perfectamente y ahora tiene que funcionar también con CoreData. Mi duda es, ¿es posible vincular de alguna manera las entidades que me cree con los NSObjects que ya tengo? o de que se inicien con ellos o algo así de manera que solo tenga que incluirlo y guardar en bbdd cuando lo necesite?

    Si no es así y tuviera que redifinir los NSObjects como NSManagedObject, hay un en concreto que lo tengo con initWithCoder porque es parte de otro objecto que se guarda en NSUserDefault. ¿Podría seguir haciendo esto? ¿No es necesario hacerlo?

    Muchas gracias por tu ayuda y te animo a seguir así.

    Un saludo!!

    • Hola Laura, ¡muchas gracias!

      Respecto a tu duda, sin conocer muy bien el caso en particular, diría de primeras que podrías sustituir la herencia de tus objetos del modelo por ManagedObject, pero no manualmente; y me explico: tendrás que separar (bien en una categoría o en una subclase) el código propio, el tuyo, ya que XCode por defecto crea su propias clases a partir de las entidades de CoreData.
      Es decir, genera tus clases del modelo con Xcode (o mogenerator) y preserva simplemente tú código del modelo aparte, para que no lo sobrescriba (si hablamos de ObjetiveC, ya que en Swift es distinto). Vamos, que yo no cambiaría la clase NSObject por ManagedObject tal cual, sino que haría un traspaso algo más manual.
      En cuanto al inicializador, no sabría qué decirte, porque le resulta extraño que una clase de tu modelo necesite un «initWithCoder:». En todo caso, esa clase no puede pertenecer a tu modelo de CoreData y habrías de mapearla de alguna forma. Es cuanto se me ocurre.

      Dime si te ha servido de ayuda.

      • Hola Miguel!!
        Si si que me ha servido de ayuda, creo que ya se como resolver el problema muchas gracias.
        Respecto al inicializador, te explico. Pongamos por ejemplo que la app muestra un lista de tiendas con mi NSObject Tienda y por otro lado tengo mi NSObject User es cuál se guarda en NSUserDefault, como es un objeto customizado necesita el «initWithCoder» para poder guardarse en NSUserDefault. La cosa es que este usuario tiene también una lista de tiendas que ha visitado, por lo que para poder guardarlo, este objecto Tienda tiene que tener también el «initWithCoder». Pero a su vez es este objeto el que necesitaba cambiar para guardar con Coredata. Es un ejemplo de más o menos el problema que tenía, pero si aplico lo que me comentas ya no tendría problema.

        Muchas gracias!!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Mi Gobierno me obliga a decirle que utilizo 'cookies' en mi página. Si continúa navegando, quiere decir que está conforme con ello.