author
Lorsque vous développez une application iOS ou bien OS X travaillant avec des flux de données JSON, vous pouvez décider de vous contenter d’utiliser ce que les SDK d’Apple proposent ou bien vous pouvez vous reposer sur des librairies qui vous facilitent le travail.
Si vous optez pour la première solution, le travail à accomplir peut se révéler complexe et fastidieux. Mieux vaut s’appuyer sur des librairies reconnues pour leur qualités telles qu’AFNetworking pour la gestion des appels HTTP ou bien encore JSONKit pour la sérialisation & désérialisation de payloads JSON.
En utilisant ces librairies vous pourrez mettre de côté une partie de la complexité, il vous restera tout de même à gérer une stratégie de mise en cache des données et vous devrez mapper les payloads JSON avec une représentation objet.
Toutefois, si vous ne souhaitez pas gérer ces problèmes à la main, il vous reste la possibilité d’utiliser la librairie RestKit qui propose:
- La consommation de données JSON depuis un serveur distant
- Le mapping entre les structures de données JSON et les objets de votre modèle de données
- Le stockage en cache de requêtes effectuées
- Le stockage en base de votre modèle métier via l’utilisation de CoreData
- L’initialisation de votre base de données
Initialisation du projet
Si vous souhaitez manipuler le code source fourni dans cet article, vous devrez utiliser CocoaPods que nous avons découvert dans un article précédent.
Avant d’utiliser CocoaPods, vous devrez créer un projet en ligne de commande pour OS X via Xcode:
Ensuite, vous aurez à créer un fichier Podfile à la racine de votre projet Xcode avec les dépendances suivantes:
platform :osx
pod 'JSONKit', '1.5pre'
pod 'LibComponentLogging-Core', '1.2.2'
pod 'LibComponentLogging-NSLog', '1.0.4'
pod 'Reachability', '3.0.0'
pod 'RestKit', '0.10.1'
pod 'Underscore.m', '0.1.0'
Puis, vous devrez exécutez la commande suivante:
pod install
Il faudra alors relancer votre projet en ouvrant le fichier *.XCodeWorkspace, plutôt que le fichier *.XCodeProject. Vous serez alors prêt à tester RestKit.
Point d’entrée du programme
RestKit est une librairie qui fonctionne par défaut en mode asynchrone. Si nous souhaitons pouvoir la tester en ligne de commande, nous devons nous assurer que le programme ne quittera pas avant la récupération et l’affichage de la réponse. Pour cela, nous allons utiliser les fonctions CFRunLoopRun, CFRunLoopStop et CFRunLoopGetMain du framework CoreFoundation qui permettent de démarrer une boucle de gestion d’événements et de l’arrêter au moment souhaité.
L’appel à la resource GitHub est délégué à la classe XBRestkitService. Une fois initialisée, cette classe propose la méthode d’instance loadArrayOfDataWithRelativePath:onLoad:onError: permettant de récupérer une liste d’objets. Dans notre cas, la liste des repositories GitHub. Elle dispose de 2 callbacks de retour permettant de traiter les données récupérées en cas de succès ou bien l’erreur retournée en cas de problème.
#import "RestKitTutorialApp.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
initializeRestKit();
NSString *relativePath = [@"/orgs/:organization/repos" interpolateWithObject:@{@"organization": @"facebook"}];
XBRestkitService *restkitService = [[XBRestkitService alloc] init];
[restkitService loadArrayOfDataWithRelativePath:relativePath
onLoad:^(NSArray *repositories) {
NSLog(@"Repository list from Network: ");
_array(repositories).each(^(GHRepository *repository) {
NSLog(@"* %@", [repository description]);
});
CFRunLoopStop(CFRunLoopGetMain());
}
onError:^(NSError *error) {
NSLog(@"%@", error);
CFRunLoopStop(CFRunLoopGetMain());
}
];
CFRunLoopRun();
}
return 0;
}
Flux de données source
Nous allons nous intéresser dans cet article aux 3 premières fonctionnalités proposées par RestKit, c’est à dire la consommation de données retournées par un serveur distant, leur mapping sur un modèle métier, ainsi que le stockage en cache des requêtes effectuées. Pour cela, nous allons utiliser l’API GitHub qui permet entre autre de lister les repositories d’une organisation via l’appel de l’URL suivante :
Dans l’URL définie ci-dessus, le terme organization est préfixé par le caractère ‘:’. Nous le verrons plus tard en détail, mais cela correspond à la définition d’une paramètre qui sera extrait de l’URL, puis utilisé dans le programme.
Voyons dans un premier temps à quoi peut ressembler un flux de données correspondant à la liste des repositories de l’organisation FaceBook disponible à l’URL suivante: https://api.github.com/orgs/facebook/repos.
[
{
"default_branch": "master",
"homepage": "http://three20.info/",
"html_url": "https://github.com/facebook/three20",
"owner": {
"login": "facebook",
"avatar_url": "https://secure.gravatar.com/avatar/193c1a93276f729041fc875cf2a20773?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-org-420.png",
"gravatar_id": "193c1a93276f729041fc875cf2a20773",
"url": "https://api.github.com/users/facebook",
"id": 69631
},
"has_downloads": false,
"created_at": "2009-02-19T06:31:51Z",
"watchers": 6961,
"has_issues": true,
"description": "Three20 is an Objective-C library for iPhone developers",
"pushed_at": "2012-10-18T09:30:54Z",
"forks": 1207,
"git_url": "git://github.com/facebook/three20.git",
"ssh_url": "git@github.com:facebook/three20.git",
"svn_url": "https://github.com/facebook/three20",
"has_wiki": true,
"master_branch": "master",
"clone_url": "https://github.com/facebook/three20.git",
"size": 4704,
"forks_count": 1207,
"fork": false,
"updated_at": "2012-10-23T19:37:21Z",
"watchers_count": 6961,
"name": "three20",
"open_issues": 252,
"url": "https://api.github.com/repos/facebook/three20",
"private": false,
"id": 132321,
"language": "Objective-C",
"mirror_url": null,
"open_issues_count": 252,
"full_name": "facebook/three20"
}
...
]
Modélisation du modèle métier
Après une rapide analyse du flux de données, nous pouvons observer que les données renvoyées sont représentées via une liste d’objets JSON décrivant chacun un repository. Chaque repository fournit en particulier un attribut owner correspondant à l’utilisateur propriétaire du repository.
Si nous voulons convertir ces données en un modèle objet, il est nécessaire de créer 2 classes dans notre modèle:
- La classe GHRepository qui représentera le repository
- Ainsi que la classe GHOwner qui représentera l’utilisateur propriétaire du repository
En Objective-C, il n’existe pas de notion de namespace, ainsi afin d’éviter tout risque de collision dans le nommage des classes, un préfixe est utilisé par convention. Puisque nous travaillons avec des classes modélisant de données en provenance de GitHub, nous utiliserons le préfixe GH.
Note: Les déclarations de synthétisation des Getters et Setters sont devenues optionnelles avec Xcode 4.5 et la version 3.1 du compilateur Clang. C’est pourquoi, elle ne sont absentes des classes d’implémentations ci-dessous.
Définition de la classe GHOwner
Interface
#import <restkit /RestKit.h>
@interface GHOwner : NSObject
@property (nonatomic, strong) NSNumber *identifier;
@property (nonatomic, strong) NSString *login;
@property (nonatomic, strong) NSString *gravatar_id;
@property (nonatomic, strong) NSString *avatar_url;
@property (nonatomic, strong) NSString *type;
+ (RKObjectMapping *)mapping;
@end
La méthode mapping permet de déclarer les mapping associés à l’objet GHOwner.
implémentation
#import "GHOwner.h"
@implementation GHOwner
+ (RKObjectMapping *)mapping {
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[self class]
usingBlock:^(RKObjectMapping *mapping) {
[mapping mapAttributes: @"login", @"gravatar_id", @"avatar_url", @"type", nil];
[mapping mapKeyPathsToAttributes: @"id", @"identifier", nil];
}];
return mapping;
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@ - %@", self.identifier, self.login];
}
@end
La méthode mapping renvoie un objet de type RKObjectMapping qui représente la définition du mapping d’un objet de type GHOwner. Le mapping est défini à l’intérieur d’un block qui fournit une instance de mapping qui est à renseigner.
L’appel à la méthode mapAttributes permet de renseigner les mapping d’attributs pour lesquels le nom de l’attribut source est le même que le nom de l’attribut de destination.
L’appel à la méthode mapKeyPathsToAttributes permet de renseigner les mapping d’attributs pour lesquels le nom de l’attribut source est différent du nom de l’attribut de destination. Dans notre exemple, il n’est pas désirable d’appeler dans la classe GHOwner l’attribut identifier du même nom que sa source, étant donné que le terme id est un mot clé du langage Objective-C. Dans le meilleur des cas, cela posera des problèmes de coloration syntaxique dans le code source.
Définition de la classe GHRepository
Interface
#import "GHOwner.h"
#import </restkit><restkit /RestKit.h>
@interface GHRepository : NSObject
@property (nonatomic, strong) NSNumber *identifier;
@property (nonatomic, strong) NSDate *created_at;
@property (nonatomic, assign) bool fork;
@property (nonatomic, strong) NSNumber *forks;
@property (nonatomic, strong) NSString *full_name;
@property (nonatomic, assign) bool has_downloads;
@property (nonatomic, assign) bool has_issues;
@property (nonatomic, assign) bool has_wiki;
@property (nonatomic, strong) NSString *git_url;
@property (nonatomic, strong) NSString *homepage;
@property (nonatomic, strong) NSString *html_url;
@property (nonatomic, strong) NSString *language;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSNumber *open_issues;
@property (nonatomic, strong) GHOwner *owner;
@property (nonatomic, strong) NSDate *pushed_at;
@property (nonatomic, strong) NSNumber *size;
@property (nonatomic, strong) NSDate *updated_at;
@property (nonatomic, strong) NSString *url;
@property (nonatomic, strong) NSNumber *watchers;
+ (RKObjectMapping *)mapping;
@end
Nous pouvons noter ici que l’attribut owner est du type GHOwner.
#import "GHRepository.h"
@implementation GHRepository
- (NSString *)description {
return [NSString stringWithFormat:@"%@ [%@ / %@ Forks / %@ Watchers / %@ issues]", self.name, self.language, self.forks, self.watchers, self.open_issues];
}
+ (RKObjectMapping *)mapping {
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[self class] usingBlock:^(RKObjectMapping *mapping) {
[mapping mapAttributes:
@"created_at", @"fork", @"forks", @"full_name", @"has_downloads", @"has_issues", @"has_wiki",
@"git_url", @"homepage", @"html_url", @"language", @"name", @"open_issues",
@"pushed_at", @"size", @"updated_at", @"url", @"watchers", nil];
[mapping mapKeyPathsToAttributes:
@"id", @"identifier",
nil];
}];
// Relationships
[mapping hasMany:@"owner" withMapping:[GHOwner mapping]];
return mapping;
}
@end
Une déclaration complémentaire est nécessaire pour mapper l’attribut source owner. Pour cela nous allons appeler la méthode hasMany:withMapping: sur l’objet mappping en lui passant le nom de l’attribut en premier paramètre, ainsi qu’un objet RKObjectMapping représentant le mapping de l’objet.
Une fois les classes du modèle définies nous pouvons nous intéresser au lancement du programme.
Configuration de RestKit
Les loggers
RestKit dispose d’un système de logging efficace. Vous pouvez simplement le paramétrer en début de programme comme suit:
// RKLogConfigureByName("RestKit/*", RKLogLevelInfo);
// RKLogConfigureByName("RestKit/UI", RKLogLevelWarning);
RKLogConfigureByName("RestKit/Network", RKLogLevelWarning);
RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelWarning);
// RKLogConfigureByName("RestKit/ObjectMapping/JSON", RKLogLevelTrace);
Comme nous pouvons le constater, il est possible de configurer les niveaux de logs des différents packages du niveau Trace (RKLogLevelTrace), le plus bas, au niveau Error (RKLogLevelError), le plus haut. RestKit dispose d’une configuration wildcard, définie par le nom: “RestKit/*”. Cette configuration wildcard permet de configurer l’ensemble de l’application via un niveau de log par défaut.
La classe ObjectManager
Une fois les niveaux de log configurés, nous devons initialiser une instance de la classe RKObjectManager qui sera utilisée pour exécuter les différentes requêtes. Toutes ces requêtes ont une même url de base. Dans notre cas: https://api.github.com.
Un objet de type RKObjectManager référence un objet de type RKClient qui sera en charge de gérer le requêtes exécutées. Cet objet de type RKClient est configurable, il est possible entre autre de lui définir une stratégie de cache. Dans notre cas, nous allons paramétrer une stratégie de cache stockant de façon permanente le résultat des requêtes dans un ficher.
RKObjectManager *objectManager = [RKObjectManager managerWithBaseURLString:@"https://api.github.com"];
objectManager.client.cachePolicy = RKRequestCachePolicyEnabled;
objectManager.client.requestCache.storagePolicy = RKRequestCacheStoragePolicyPermanently;
// [objectManager.client.requestCache invalidateAll];
Si vous souhaitez invalider le cache, vous pouvez appeler la méthode invalidateAll sur la référence requestCache de l’object client.
La classe ObjectMapping
RestKit met à disposition un objet de type RKObjetMappingProvider permettant déclarer les différents mappings utilisé par la librairie. Une instance peut être obtenue via l’appel de la propriété mappingProvider sur l’instance partagée de la classe RKObjectManager.
// Github date format: 2012-07-05T09:43:24Z
// Already available in Restkit default formatters
[RKObjectMapping addDefaultDateFormatter: [NSDateFormatter initWithDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]];
RKObjectMappingProvider *omp = [RKObjectManager sharedManager].mappingProvider;
[omp addObjectMapping:[GHRepository mapping]];
[omp setObjectMapping:[GHRepository mapping] forResourcePathPattern:@"/orgs/:organization/repos"];
Dans un premier temps, nous déclarons un object NSDateFormatter. Il permettra de parser les dates au format texte fournies dans les données JSON renvoyées par GitHub. (Note: Ce format de date est supporté par défaut dans RestKit, mais nous l’enregistrons ici pour l’exemple).
Dans un second temps, nous enregistrons les mappings correspondant au modèle de données avec lequel nous travaillons, c’est à dire, la classe GHRepository. Les mapping sont toujours enregistrés pour un pattern d’URL donné.
Exécution de l’appel
Pour faciliter la réutilisation du code qui exécutera la requête, nous allons encapsuler celui-ci dans une méthode comme suit:
// A définir dans un fichier d'en-tête
typedef void(^XBRequestDidFailWithErrorBlock)(NSError *error);
typedef void(^XBRequestDidLoadObjectsBlock)(NSArray *objects);
- (void)loadArrayOfDataWithRelativePath:(NSString *)url
onLoad:(XBRequestDidLoadObjectsBlock)loadBlock
onError:(XBRequestDidFailWithErrorBlock)failBlock
{
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:url usingBlock:^(RKObjectLoader *loader) {
loader.onDidLoadObjects = loadBlock;
loader.onDidFailWithError = failBlock;
loader.onDidFailLoadWithError = failBlock;
loader.onDidLoadResponse = ^(RKResponse *response) {
[self fireErrorBlock:failBlock onErrorInResponse:response];
};
}];
}
- (void)fireErrorBlock:(RKRequestDidFailLoadWithErrorBlock)failBlock onErrorInResponse:(RKResponse *)response {
if (![response isOK]) {
id parsedResponse = [response parsedBody:NULL];
NSString *errorText = nil;
if ([parsedResponse isKindOfClass:[NSDictionary class]]) {
errorText = [parsedResponse objectForKey:@"error"];
}
if (errorText) {
NSError *errorWithMessage = [NSError errorWithDomain:XBErrorDomain
code:[response statusCode]
userInfo:[NSDictionary dictionaryWithObject:errorText
forKey:NSLocalizedDescriptionKey]];
failBlock(errorWithMessage);
}
}
}
Les différents cas d’erreurs sont gérés via la configuration de l’objet loader lors de l’appel de la ressource. La méthode fireErrorBlock:onErrorInResponse: permet d’appeler le block failBlock en cas de détection d’erreur dans la body de la réponse.
Une fois les méthodes de requêtage définies, nous pouvons les utiliser pour charger les données comme suit:
XBRestkitService *restkitService = [[XBRestkitService alloc] init];
[restkitService loadArrayOfDataWithRelativePath:relativePath
onLoad:^(NSArray *repositories) {
NSLog(@"Repository list from Network: ");
_array(repositories).each(^(GHRepository *repository) {
NSLog(@"* %@", [repository description]);
});
CFRunLoopStop(CFRunLoopGetMain()); // Appel permettant de quitter le programme
}
onError:^(NSError *error) {
NSLog(@"%@", error);
CFRunLoopStop(CFRunLoopGetMain()); // Appel permettant de quitter le programme
}
];
En cas d’erreur, nous nous contentons de logger l’erreur, sinon nous listons le nom des différents repositories appartenant à l’organisation. Nous utilisons ici la librairie Underscore.m qui permet d’utiliser une approche plus fonctionnelle dans le traitement des tableaux.
Conclusion
Nous pouvons voir à travers cet exemple à quel point RestKit peut se révéler précieux. L’usage de la librairie permet d’éviter se focaliser trop sur la technique en fournissant les briques de base nécessaire à tout programme connecté utilisant des structures de données JSON. RestKit permet toutefois d’aller beaucoup plus loin puisqu’il s’intègre avec CoreData ou bien encore fournit de quoi se connecter à des API sécurisées par le protocole OAuth. Dans une seconde partie, nous verrons que RestKit dispose également de tout le matériel nécessaire permettant de créer rapidement des interfaces graphiques de type liste pour iOS.
Code source
Le code source de l’article est disponible sur le repository GitHub restkit-tutorial.