Obtener tweets y sincronizarlos con nuestra caché de Core Data
En la primera parte de esta serie de artículos vimos como crear el modelo de datos para nuestra timeline de Twitter.
En esta segunda parte de la serie vamos implementar una clase que encapsule la funcionalidad de la timeline: obtener tweets y sincronizarlos con nuestra caché de Core Data. Además, añadiremos algunos métodos a nuestras clases TGRTweet y TGRTwitterUser para que puedan importar un diccionario JSON. Todo esto al mismo tiempo que repasamos algunos conceptos de Core Data y de la API de Twitter.
Si no habéis completado la primera parte o habéis perdido el proyecto, no os preocupéis. Podéis descargar el código fuente aquí.
Correcciones a la primera parte
Antes de empezar, vamos a corregir un par de cosas que pasamos por alto en la primera parte.
Lo primero de todo, en iOS 6 Twitter.framework ha sido deprecado, por lo que debemos eliminar la dependencia de nuestro proyecto y la sustituirla por Social.framework.
La otra cosa que tenemos que corregir, es una ineficiencia que hay en el modelo de datos. Si recordáis, ambas entidades TGRTweet y TGRTwitterUser contienen un atributo ‘identifier’ de tipo ‘String’. En realidad Twitter usa un entero de 64 bits como identificador pero, para mantener la compatibilidad con algunos lenguajes (ej. Javascript), también proporciona este dato como un string.
Es importante corregir esta ineficiencia, ya que vamos a usar los identificadores tanto para ordenar como para realizar búsquedas. Para ello, abrimos el modelo de datos y seleccionamos el atributo ‘identifier’ de la entidad TGRTweet. Cambiamos el tipo a ‘Integer 64′ y nos aseguramos de desactivar el valor por defecto. Realizamos el mismo proceso con el atributo ‘identifier’ de la entidad TGRTwitterUser.
A continuación abrimos TGRTweet.h y TGRTwitterUser.h y cambiamos
@property (nonatomic, retain) NSString * identifier;
por
@property (nonatomic, retain) NSNumber * identifier;
Trabajando con timelines
La API de Twitter proporciona varios métodos que devuelven una timeline. En nuestro caso el que nos interesa es GET statuses/home_timeline, ya queremos mostrar los tweets y retweets de los usuarios a los que sigue el usuario autenticado.
Debido al tamaño que puede tener una timeline, la API de Twitter limita el número de tweets que se pueden obtener en una única petición. Es decir, para construir una timeline completa debemos realizar varias peticiones, iterando sobre los resultados.
En lugar de utilizar paginación, la API de Twitter utiliza una técnica denominada ‘cursoring’. Esta técnica consiste en pedir secciones de la timeline relativas a los identificadores de los tweets que ya han sido procesados. Esto se consigue usando los parámetros max_id y since_id en la petición.
Si se especifica un valor para el parámetro max_id, la petición devolverá los tweets de la timeline con un identificador menor (es decir, más antiguo) o igual que el valor especificado.
Por el contrario, si se especifica un valor para el parámetro since_id, la petición devolverá los tweets de la timeline con un identificador mayor (es decir, más reciente) que el valor especificado.
Utilizando un ejemplo más visual, cuando usamos el gesto pull-to-refresh en nuestra timeline, la aplicación de Twitter para iOS utiliza el identificador del tweet más reciente que tiene en la caché como valor del parámetro since_id. Por el contrario, si hacemos scroll hasta el tweet más antiguo de la caché, la aplicación utiliza su identificador como valor del parámetro max_id para pedir tweets más antiguos que este.
Creando nuestra clase Timeline
Añadimos un nuevo fichero al proyecto, seleccionamos la plantilla ‘Objective-C class’ e introducimos ‘TGRTimeline’ como nombre de la clase y ‘NSObject’ como súper-clase.
Abrimos TGRTimeline.h en el editor e introducimos el siguiente código:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Como podéis ver, nuestra timeline tiene las siguientes propiedades:
managedObjectContextes el contexto de Core Data que vamos a utilizar para guardar y recuperar los tweets cacheados. Además, nuestro ViewController va a utilizar este contexto obtener los tweets y mostrarlos en pantalla.loadingindica si hay alguna petición a la API de Twitter en curso. Esta propiedad va a servir para que el ViewController pueda mostrar un indicador de actividad mientras se están obteniendo tweets.
Y además expone los siguientes métodos:
initWithAccount:inicializa la timeline con la cuenta de Twitter que se va a utilizar para autenticar las peticiones al servicio.loadNewTweetsWithCompletionHandler:obtiene los tweets más recientes del usuario autenticado.loadOldTweetsWithCompletionHandler:obtiene tweets más antiguos a los que haya guardados en caché en ese momento.
Estos últimos dos métodos realizan peticiones a la API de Twitter y por lo tanto son asíncronos. Tienen como parámetro un bloque que será llamado cuando la petición se haya completado y que recibe cualquier error que haya podido ocurrir.
Implementando TGRTimeline
Comenzamos añadiendo los imports necesarios y declarando una interfaz privada en TGRTimeline.m:
| 1 2 3 4 5 6 7 8 9 10 |
|
Como podéis ver hemos declarado la propiedad loading como de lectura/escritura y además hemos añadido una propiedad para guardar la cuenta de Twitter que utiliza la timeline.
A continuación añadimos la implementación de initWithAccount:. Lo único que hacemos es guardar la referencia a la cuenta que se pasa como parámetro.
| 1 2 3 4 5 6 7 8 9 10 |
|
La implementación de loadNewTweetsWithCompletionHandler: y loadOldTweetsWithCompletionHandler: es muy similar. Tan sólo utilizan métodos distintos para confeccionar la petición a la API de Twitter.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Ambos métodos comprueban si ya hay una petición en curso, en cuyo caso terminan inmediatamente devolviendo NO.
Lo siguiente que hacen es confeccionar la petición, utilizando requestForNewTweets o requestForOldTweets. De momento podemos dejar la implementación de estos métodos vacía. Más adelante veremos como implementarlos.
| 1 2 3 4 5 6 7 8 9 |
|
Por último, ambos métodos llaman a loadTweetsWithRequest:completionHandler: pasando como parámetros la petición y el bloque que se ha de invocar cuando la petición se haya completado.
Implementando loadTweetsWithRequest:completionHandler:
La cosa se pone interesante con la implementación de este método, que es el que ejecuta la petición e importa los resultados a nuestra caché.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Para enviar la petición, utilizamos performRequestWithHandler, pasando como parámetro un bloque que será invocado cuando esta se haya completado. En este bloque, hacemos lo siguiente:
- Convertimos el JSON en un array de diccionarios, utilizando la clase
NSJSONSerialization. - Guardamos los tweets en nuestra caché de Core Data. Como no estamos en el hilo principal, utilizamos el método
performBlockpara asegurarnos de que la importación se realiza en el hilo adecuado. Más adelante veremos como implementarimportTweets. - Finalmente invocamos el bloque que nos han pasado como parámetro, utilizando
dispatch_asyncpara que se ejecute en el hilo principal.
Operaciones básicas de Core Data
Antes de implementar los métodos que nos faltan en TGRTimeline, vamos a repasar algunas operaciones básicas de Core Data.
Si nos ceñimos a la documentación de Core Data, el código necesario para insertar un tweet sería el siguiente:
| 1 2 |
|
Personalmente, creo que sería mejor si pudiéramos insertar un tweet de esta manera:
| 1 |
|
Y todavía sería aún mejor si pudiéramos insertar el tweet e importar el diccionario JSON al mismo tiempo:
| 1 2 |
|
Hacer una consulta con Core Data puede llegar a ser algo pesado. Por ejemplo, para obtener todos los tweets de nuestra timeline en orden cronológico inverso, tendríamos que hacer lo siguiente:
| 1 2 3 4 5 6 7 8 |
|
Estaría bien poder encapsular la creación de objetos NSFetchRequest, de manera que pudiéramos hacer lo mismo con el siguiente código:
| 1 2 |
|
Vamos a añadir algunas cosas a nuestro modelo de datos para hacer esto posible.
Añadiendo funcionalidad básica al modelo de datos
Comenzamos creando una nueva clase, introduciendo TGRManagedObject como nombre y NSManagedObject como súper-clase.
Abrimos TGRManagedObject.h e introducimos el siguiente código:
| 1 2 3 4 5 6 7 8 9 10 11 |
|
A continuación abrimos TGRManagedObject.m e introducimos el siguiente código:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Gracias a que en la primera parte utilizamos el mismo nombre para la entidad y su clase correspondiente, la implementación de entityName es trivial. Tan sólo tenemos que llamar a NSStringFromClass para obtener el nombre de la clase como un string.
El método fetchRequest devuelve un objeto NSFetchRequest configurado para realizar consultas sobre la entidad con la que se corresponde la clase.
Los métodos insertNewObjectInManagedObjectContext: y importFromDictionary:inManagedObjectContext: facilitan la inserción de objetos, tal y cómo hemos visto en la sección anterior.
El método importFromDictionary:inManagedObjectContext: se apoya en importValuesFromDictionary: para asignar los valores del diccionario a las propiedades del objeto. Este método tiene que ser implementado por las subclases.
Completando la implementación de TGRTwitterUser
Una vez que tenemos lista la clase TGRManagedObject, tenemos que hacer los cambios necesarios para que esta sea la súper-clase de TGRTwitterUser.
| 1 2 3 4 5 6 |
|
A continuación añadimos los siguientes métodos a la interfaz de TGRTwitterUser:
| 1 2 3 4 |
|
Estos métodos son necesarios para comprobar si ya tenemos un determinado usuario en la caché antes de asociarlo con el tweet correspondiente.
La implementación de fetchRequestForTwitterUserWithIdentifier: quedaría como sigue:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Como este método va a ser invocado muy frecuentemente (cada vez que insertamos un tweet), creamos una plantilla para el predicado en la primera llamada, y la reutilizamos en las siguientes llamadas sustituyendo la variable por el identificador que se pasa como parámetro.
La implementación de twitterUserWithIdentifier:inManagedObjectContext: utiliza el método anterior para obtener el objeto NSFetchRequest y a continuación ejecuta la consulta en el contexto que se pasa como parámetro.
| 1 2 3 4 5 |
|
Por último, consultamos la documentación del API de Twitter para saber como obtener los campos que necesitamos en la implementación de importValuesFromDictionary:.
| 1 2 3 4 5 6 7 |
|
Completando la implementación de TGRTweet
Al igual que con TGRTwitterUser, necesitamos hacer que TGRManagedObject sea la súper-clase de TGRTweet.
| 1 2 3 4 5 6 |
|
A continuación añadimos los siguientes métodos a la interfaz de TGRTweet:
| 1 2 3 4 |
|
El primer método, fetchRequestForAllTweets, devuelve un objeto NSFetchRequest configurado para obtener todos los tweets en orden cronológico inverso.
| 1 2 3 4 5 6 7 8 9 10 |
|
El método firstTweetInManagedObjectContext: sirve para obtener el tweet más antiguo que existe en la caché.
| 1 2 3 4 5 6 7 8 9 10 11 |
|
Tan sólo necesitamos ordenar por identificador de manera ascendente, configurando la consulta para obtener el primer elemento.
Por el contrario, el método lastTweetInManagedObjectContext: sirve para obtener el tweet más reciente que existe en la caché.
| 1 2 3 4 5 6 7 8 9 10 11 |
|
Lo mismo que en el método anterior, pero esta vez ordenamos de manera descendente, para obtener el tweet más reciente.
La implementación de importValuesFromDictionary: es un poco más compleja que en TGRTwitterUser. Podéis consultar la documentación del API de Twitter para saber que campos del JSON se corresponden con los atributos de nuestro objeto TGRTweet.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
Lo primero que hace el método es comprobar si está procesando un tweet o un retweet, en cuyo caso obtiene los campos de la clave @"retweeted_status". Lo siguiente que hace es obtener la fecha de publicación, transformando la cadena de fecha que viene en el JSON en un objeto NSDate. A continuación busca el usuario y si no lo encuentra lo crea, para finalmente asociarlo al tweet. Por último, si está procesado un retweet, realiza la misma operación con el usuario que ha re-tuiteado.
Para implementar el método que convierte una cadena de fecha en un objeto NSDate utilizamos la clase NSDateFormatter.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Completando la implementación de TGRTimeline
Una vez que hemos completado la infraestructura de nuestro modelo de datos, estamos listos para terminar de implementar nuestra clase TGRTimeline.
Lo primero que vamos a hacer es definir una constante con la URL del método de la API de Twitter que vamos a utilizar.
| 1 |
|
En la implementación de el método requestForNewTweets debemos construir una petición para obtener tweets más recientes de los que tenemos en caché.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Lo primero que hace el método es preparar un diccionario con los parámetros de la petición. Por un lado, queremos que la timeline incluya retweets. Además, queremos obtener tweets más recientes a los que ya tenemos en caché, por lo que asignamos el identificador del tweet más reciente que tenemos como valor del parámetro @"since_id". A continuación construimos la petición y nos aseguramos de pasarle nuestra cuenta de usuario.
En la implementación del método requestForOldTweets vamos a construir una petición para obtener tweets más antiguos a los que ya tenemos en caché.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Como podéis ver, lo que hace este método es obtener el tweet más antiguo y calcular el identificador que hay que configurar como parámetro @"max_id" de la petición. El resto es igual que en el método anterior.
La implementación del método importTweets: es trivial. Tan sólo tenemos que iterar sobre el array de diccionarios y crear objetos TGRTweet utilizando el método importFromDictionary:inManagedObjectContext:. Además, tenemos que persistir los cambios realizados en el contexto de Core Data.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
El stack de Core Data
El stack de Core Data se compone de tres partes principales: NSPersistentStoreCoordinator, NSManagedObjectModel y NSManagedObjectContext. Estos tres objetos trabajan conjuntamente, permitiéndonos consultar e insertar objetos NSManagedObject.
Hasta ahora hemos utilizado el objeto NSManagedObjectContext, pero necesitamos incorporar el código necesario para crearlo, junto con el resto del stack.
En la mayoría de los ejemplos de Core Data, la creación del stack se realiza en el delegado de la aplicación, con objeto de que este sea accesible al resto de componentes de la aplicación. Pero en nuestro caso, lo que tiene sentido es encapsularlo dentro de la clase TGRTimeline.
Lo primero que tenemos que hacer es añadir unas cuantas propiedades a la interfaz privada de TGRTimeline.
| 1 2 3 4 5 6 7 8 9 10 11 |
|
Como podéis ver, hemos añadido una versión de lectura/escritura de la propiedad managedObjectContext. Además hemos añadido las propiedades correspondientes al resto del stack, managedObjectModel y persistentStoreCoordinator.
En lugar de crear el stack dentro del método initWithAccount:, vamos a utilizar la técnica de inicialización diferida, creando cada parte del stack la primera vez que se accede a ella.
Para ello necesitamos proporcionar nuestra propia implementación de los métodos que usan las propiedades para acceder a los objetos del stack.
Como parte del proceso de compilación de la aplicación, los archivos ‘xcdatamodel’ se compilan en archivos con extensión ‘mom’ o ‘momd’, según haya una o varias versiones del mismo modelo. Vamos a usar este archivo para construir nuestro objeto NSManagedObjectModel.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
El objeto NSPersistentStoreCoordinator está en la parte inferior del stack, y es responsable de la persistencia de los datos en un repositorio.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Lo primero que hace el método es construir la ruta que va a tener nuestro repositorio. El siguiente paso es construir el objeto NSPersistentStoreCoordinator utilizando nuestro objeto NSManagedObjectModel. Por último, añadimos un repositorio de tipo SQLite con la ruta que hemos construido previamente.
Finalmente vamos a ver como se construye nuestro objeto NSManagedObjectContext.
| 1 2 3 4 5 6 7 8 9 |
|
Como podéis ver, el objeto NSManagedObjectContext se construye para que sea accesible sólo desde el hilo principal y, a continuación, se asocia con nuestro objeto NSPersistentStoreCoordinator.
Código fuente
Ya tenemos nuestra clase TGRTimeline terminada. Podéis descargar el código fuente de todo lo que hemos hecho aquí.
En el próximo artículo vamos a implementar un ViewController que muestra la timeline en una UITableView. Veremos como adaptar la altura de las filas al tamaño del texto que hay en el tweet, descargar las imágenes de perfil de los usuarios, utilizar UIRefreshControl, etc.
Mientras tanto, si tenéis preguntas o comentarios podéis encontrarme en Twitter como @gonzalezreal.
Acerca del autor
Guillermo González, artesano del software, amante de los cómics y aficionado al cine. Líder de desarrollo móvil para PopCha! Podéis encontrarme en Twitter como @gonzalezreal.







[...] en la primera parte creamos el modelo de datos para la caché de nuestra timeline. En la segunda parte, implementamos la clase TGRTimeline para obtener tweets y sincronizarlos con la [...]