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 antiguoo 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
#import <Foundation/Foundation.h>
#import <Accounts/Accounts.h>
#import <CoreData/CoreData.h>
 
@interface TGRTimeline : NSObject
 
@property (strong, nonatomic, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, readonly, getter = isLoading) BOOL loading;
 
- (id)initWithAccount:(ACAccount *)account;
 
- (BOOL)loadNewTweetsWithCompletionHandler:(void (^)(NSError *error))completionHandler;
 
- (BOOL)loadOldTweetsWithCompletionHandler:(void (^)(NSError *error))completionHandler;
 
@end
view raw TGRTimeline.h This Gist brought to you by GitHub.

Como podéis ver, nuestra timeline tiene las siguientes propiedades:

  • managedObjectContext es 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.
  • loading indica 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
#import "TGRTimeline.h"
#import "TGRTweet.h"
#import <Social/Social.h>
 
@interface TGRTimeline ()
 
@property (nonatomic, readwrite) BOOL loading;
@property (strong, nonatomic) ACAccount *account;
 
@end
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
@implementation TGRTimeline
 
- (id)initWithAccount:(ACAccount *)account
{
self = [super init];
if (self) {
_account = account;
}
return self;
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
- (BOOL)loadNewTweetsWithCompletionHandler:(void (^)(NSError *error))completionHandler
{
if (self.loading) {
return NO;
}
SLRequest *request = [self requestForNewTweets];
[self loadTweetsWithRequest:request completionHandler:completionHandler];
return YES;
}
 
- (BOOL)loadOldTweetsWithCompletionHandler:(void (^)(NSError *error))completionHandler
{
if (self.loading) {
return NO;
}
SLRequest *request = [self requestForOldTweets];
[self loadTweetsWithRequest:request completionHandler:completionHandler];
return YES;
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
- (SLRequest *)requestForNewTweets
{
return nil;
}
 
- (SLRequest *)requestForOldTweets
{
return nil;
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
- (void)loadTweetsWithRequest:(SLRequest *)request completionHandler:(void (^)(NSError *error))completionHandler
{
self.loading = YES;
[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if (!error) {
id response = [NSJSONSerialization JSONObjectWithData:responseData
options:0
error:NULL];
if ([response isKindOfClass:[NSArray class]]) {
[self.managedObjectContext performBlock:^{
[self importTweets:response];
}];
}
else {
NSLog(@"Error: %@", response[@"errors"][0][@"message"]);
}
}
dispatch_async(dispatch_get_main_queue(), ^{
self.loading = NO;
completionHandler(error);
});
}];
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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 performBlock para asegurarnos de que la importación se realiza en el hilo adecuado. Más adelante veremos como implementar importTweets.
  • Finalmente invocamos el bloque que nos han pasado como parámetro, utilizando dispatch_async para 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
TGRTweet *tweet = [NSEntityDescription insertNewObjectForEntityForName:@"TGRTweet"
inManagedObjectContext:self.managedObjectContext];

Personalmente, creo que sería mejor si pudiéramos insertar un tweet de esta manera:

1
TGRTweet *tweet = [TGRTweet insertNewObjectInManagedObjectContext:self.managedObjectContext];

Y todavía sería aún mejor si pudiéramos insertar el tweet e importar el diccionario JSON al mismo tiempo:

1 2
TGRTweet *tweet = [TGRTweet importFromDictionary:jsonTweet
inManagedObjectContext:self.managedObjectContext];

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
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"TGRTweet"];
[fetchRequest setFetchBatchSize:25];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"identifier"
ascending:NO];
[fetchRequest setSortDescriptors:@[sortDescriptor]];
 
NSArray *tweets = [self.managedObjectContext executeFetchRequest:fetchRequest error:NULL];
view raw FetchTweets.m This Gist brought to you by GitHub.

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
NSFetchRequest *fetchRequest = [TGRTweet fetchRequestForAllTweets];
NSArray *tweets = [self.managedObjectContext executeFetchRequest:fetchRequest error:NULL];

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
@interface TGRManagedObject : NSManagedObject
 
+ (NSString *)entityName;
+ (NSFetchRequest *)fetchRequest;
 
+ (id)insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)context;
+ (id)importFromDictionary:(NSDictionary *)dictionary inManagedObjectContext:(NSManagedObjectContext *)context;
 
- (void)importValuesFromDictionary:(NSDictionary *)dictionary;
 
@end

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
+ (NSString *)entityName
{
return NSStringFromClass([self class]);
}
 
+ (NSFetchRequest *)fetchRequest
{
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:[self entityName]];
[fetchRequest setFetchBatchSize:25];
return fetchRequest;
}
 
+ (id)insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)context
{
return [NSEntityDescription insertNewObjectForEntityForName:[self entityName]
inManagedObjectContext:context];
}
 
+ (id)importFromDictionary:(NSDictionary *)dictionary inManagedObjectContext:(NSManagedObjectContext *)context
{
TGRManagedObject *object = [self insertNewObjectInManagedObjectContext:context];
[object importValuesFromDictionary:dictionary];
return object;
}
 
- (void)importValuesFromDictionary:(NSDictionary *)dictionary
{
// Must be overridden by subclasses
}

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
#import "TGRManagedObject.h"
 
@class TGRTweet;
 
@interface TGRTwitterUser : TGRManagedObject
...

A continuación añadimos los siguientes métodos a la interfaz de TGRTwitterUser:

1 2 3 4
+ (NSFetchRequest *)fetchRequestForTwitterUserWithIdentifier:(NSNumber *)identifier;
 
+ (id)twitterUserWithIdentifier:(NSNumber *)identifier
inManagedObjectContext:(NSManagedObjectContext *)context;

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
+ (NSFetchRequest *)fetchRequestForTwitterUserWithIdentifier:(NSNumber *)identifier
{
static dispatch_once_t onceToken;
static NSPredicate *predicateTemplate;
dispatch_once(&onceToken, ^{
predicateTemplate = [NSPredicate predicateWithFormat:@"identifier == $IDENTIFIER"];
});
NSPredicate *predicate = [predicateTemplate predicateWithSubstitutionVariables:
[NSDictionary dictionaryWithObject:identifier forKey:@"IDENTIFIER"]];
NSFetchRequest *fetchRequest = [self fetchRequest];
[fetchRequest setPredicate:predicate];
return fetchRequest;
}

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
+ (id)twitterUserWithIdentifier:(NSNumber *)identifier inManagedObjectContext:(NSManagedObjectContext *)context
{
NSFetchRequest *fetchRequest = [self fetchRequestForTwitterUserWithIdentifier:identifier];
return [[context executeFetchRequest:fetchRequest error:NULL] lastObject];
}

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
- (void)importValuesFromDictionary:(NSDictionary *)dictionary
{
self.identifier = dictionary[@"id"];
self.name = dictionary[@"name"];
self.screenName = dictionary[@"screen_name"];
self.imageLink = dictionary[@"profile_image_url"];
}

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
#import "TGRManagedObject.h"
 
@class TGRTwitterUser;
 
@interface TGRTweet : TGRManagedObject
...
view raw TGRTweet.h This Gist brought to you by GitHub.

A continuación añadimos los siguientes métodos a la interfaz de TGRTweet:

1 2 3 4
+ (NSFetchRequest *)fetchRequestForAllTweets;
 
+ (id)firstTweetInManagedObjectContext:(NSManagedObjectContext *)context;
+ (id)lastTweetInManagedObjectContext:(NSManagedObjectContext *)context;
view raw TGRTweet.h This Gist brought to you by GitHub.

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
+ (NSFetchRequest *)fetchRequestForAllTweets
{
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"identifier"
ascending:NO];
NSFetchRequest *fetchRequest = [self fetchRequest];
[fetchRequest setSortDescriptors:@[sortDescriptor]];
return fetchRequest;
}
view raw TGRTweet.m This Gist brought to you by GitHub.

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
+ (id)firstTweetInManagedObjectContext:(NSManagedObjectContext *)context
{
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"identifier"
ascending:YES];
NSFetchRequest *fetchRequest = [self fetchRequest];
[fetchRequest setSortDescriptors:@[sortDescriptor]];
[fetchRequest setFetchLimit:1];
return [[context executeFetchRequest:fetchRequest error:NULL] lastObject];
}
view raw TGRTweet.m This Gist brought to you by GitHub.

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
+ (id)lastTweetInManagedObjectContext:(NSManagedObjectContext *)context
{
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"identifier"
ascending:NO];
NSFetchRequest *fetchRequest = [self fetchRequest];
[fetchRequest setSortDescriptors:@[sortDescriptor]];
[fetchRequest setFetchLimit:1];
return [[context executeFetchRequest:fetchRequest error:NULL] lastObject];
}
view raw TGRTweet.m This Gist brought to you by GitHub.

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
- (void)importValuesFromDictionary:(NSDictionary *)dictionary
{
self.identifier = dictionary[@"id"];
NSDictionary *retweetedStatus = dictionary[@"retweeted_status"];
NSString *dateString = nil;
NSDictionary *userDictionary = nil;
NSDictionary *retweetedByDictionary = nil;
if (retweetedStatus) {
self.text = retweetedStatus[@"text"];
dateString = retweetedStatus[@"created_at"];
userDictionary = retweetedStatus[@"user"];
retweetedByDictionary = dictionary[@"user"];
}
else {
self.text = dictionary[@"text"];
dateString = dictionary[@"created_at"];
userDictionary = dictionary[@"user"];
}
self.publicationDate = [[self class] dateFromTwitterDate:dateString];
TGRTwitterUser *user = [TGRTwitterUser twitterUserWithIdentifier:userDictionary[@"id"]
inManagedObjectContext:self.managedObjectContext];
if (!user) {
user = [TGRTwitterUser importFromDictionary:userDictionary
inManagedObjectContext:self.managedObjectContext];
}
self.user = user;
if (retweetedByDictionary) {
TGRTwitterUser *retweetedBy = [TGRTwitterUser twitterUserWithIdentifier:retweetedByDictionary[@"id"]
inManagedObjectContext:self.managedObjectContext];
if (!retweetedBy) {
retweetedBy = [TGRTwitterUser importFromDictionary:retweetedByDictionary
inManagedObjectContext:self.managedObjectContext];
}
self.retweetedBy = retweetedBy;
}
}
view raw TGRTweet.m This Gist brought to you by GitHub.

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
+ (NSDate *)dateFromTwitterDate:(NSString *)dateString
{
static dispatch_once_t onceToken;
static NSDateFormatter *dateFormatter;
dispatch_once(&onceToken, ^{
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
[dateFormatter setDateFormat:@"eee MMM dd HH:mm:ss ZZZZ yyyy"];
});
return [dateFormatter dateFromString:dateString];
}
view raw TGRTweet.m This Gist brought to you by GitHub.

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
static NSString * const kHomeTimelineURL = @"https://api.twitter.com/1.1/statuses/home_timeline.json";
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
- (SLRequest *)requestForNewTweets
{
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[@"include_rts"] = @"true";
TGRTweet *lastTweet = [TGRTweet lastTweetInManagedObjectContext:self.managedObjectContext];
if (lastTweet) {
parameters[@"since_id"] = [lastTweet.identifier stringValue];
}
SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodGET
URL:[NSURL URLWithString:kHomeTimelineURL]
parameters:parameters];
request.account = self.account;
return request;
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
- (SLRequest *)requestForOldTweets
{
TGRTweet *firstTweet = [TGRTweet firstTweetInManagedObjectContext:self.managedObjectContext];
if (!firstTweet) {
NSLog(@"requestForOldTweets shouldn't be called when the cache is empty");
return nil;
}
NSNumber *maxIdentifier = [NSNumber numberWithLongLong:[firstTweet.identifier longLongValue] - 1];
NSDictionary *parameters = @{
@"include_rts" : @"true",
@"max_id" : [maxIdentifier stringValue]
};
SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodGET
URL:[NSURL URLWithString:kHomeTimelineURL]
parameters:parameters];
request.account = self.account;
return request;
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
- (void)importTweets:(NSArray *)tweets
{
for (NSDictionary *tweetDictionary in tweets) {
[TGRTweet importFromDictionary:tweetDictionary inManagedObjectContext:self.managedObjectContext];
}
NSError *error = nil;
BOOL succeeded = [self.managedObjectContext save:&error];
if (!succeeded) {
NSLog(@"Error saving timeline: %@", [error localizedDescription]);
}
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
@interface TGRTimeline ()
 
@property (nonatomic, readwrite) BOOL loading;
@property (strong, nonatomic) ACAccount *account;
 
// Core Data stack
@property (strong, nonatomic, readwrite) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
 
@end
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
- (NSManagedObjectModel *)managedObjectModel
{
if (!_managedObjectModel) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"Twitter" ofType:@"mom"];
if (!path) {
path = [[NSBundle mainBundle] pathForResource:@"Twitter" ofType:@"momd"];
}
NSURL *url = [NSURL fileURLWithPath:path];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:url];
}
return _managedObjectModel;
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (!_persistentStoreCoordinator) {
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
path = [path stringByAppendingPathComponent:self.account.username];
NSURL *url = [NSURL fileURLWithPath:path];
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
NSError *error = nil;
[_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:url
options:nil
error:&error];
if (error) {
NSLog(@"Error creating the persistent store coordinator: %@", [error localizedDescription]);
}
}
return _persistentStoreCoordinator;
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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
- (NSManagedObjectContext *)managedObjectContext
{
if (!_managedObjectContext) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return _managedObjectContext;
}
view raw TGRTimeline.m This Gist brought to you by GitHub.

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.

One Response to Implementando una timeline de Twitter con Core Data (Parte II), por @gonzalezreal

  1. [...] 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 [...]