Intégration de RestKit avec une UITableView

Intégration de RestKit avec une UITableView

Nous avons découvert dans un précédent article comment utiliser RestKit pour récupérer des structures de données JSON depuis une ressource HTTP et les mapper sur un modèle métier. Nous allons voir dans ce nouvel article comment adapter le code existant pour afficher les résultats dans une UITableView iOS. ...

GitHub

Nous avons découvert dans un précédent article comment utiliser RestKit pour récupérer des structures de données JSON depuis une ressource HTTP et les mapper sur un modèle métier. Nous allons voir dans ce nouvel article comment adapter le code existant pour afficher les résultats dans une UITableView iOS. Pour cela, nous allons continuer à travailler avec les APIs GitHub, et nous fixer pour objectif d’afficher les utilisateurs d’une organisation.

Notes:

  • La version actuelle de RestKit est la 0.20. Cet article met en oeuvre la version 0.10.

Installation

Dans un premier temps, nous allons créer un projet adapté pour iOS via Xcode. Vous devrez sélectionner dans l’assistant de création de projet une application pour iOS avec un template de type Empty Application.

Dans un second temps, nous allons cibler dans notre fichier de Podfile la plateforme iOS, puis ajouter une dépendance appelée SDWebImage qui sera présentée plus tard:

platform :ios

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'
pod 'SDWebImage',                       '2.6'

Les autres dépendances ont été ajoutées lors de notre précédent article. Pour plus d’informations sur l’utlisation de CocoaPods, c’est par ici

Description de l’API GitHub

En complément de l’accès au listing des repositories d’une organisation, nous allons utiliser une seconde ressource de l’API GitHub qui permet de lister les utilisateurs d’une organisation. L’URL est la suivante:

https://api.github.com/orgs/:organization/public_members

Pour une organisation telle que xebia-france, la liste des utilisateurs ressemblera à cela:

[
  {
    "url": "https://api.github.com/users/adutra",
    "login": "adutra",
    "avatar_url": "https://secure.gravatar.com/avatar/e96398d35fcd2cb3df072bcb28c9c917?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png",
    "gravatar_id": "e96398d35fcd2cb3df072bcb28c9c917",
    "id": 463876
  },
  {
    "url": "https://api.github.com/users/akinsella",
    "login": "akinsella",
    "avatar_url": "https://secure.gravatar.com/avatar/0b9d9365c299078a5cd97cfb9861de37?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png",
    "gravatar_id": "0b9d9365c299078a5cd97cfb9861de37",
    "id": 322456
  },
  {
    "url": "https://api.github.com/users/aurelienmaury",
    "login": "aurelienmaury",
    "avatar_url": "https://secure.gravatar.com/avatar/15706981ae8b52786c1587170bb53da6?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png",
    "gravatar_id": "15706981ae8b52786c1587170bb53da6",
    "id": 191001
  },
  ...
]

Le modèle métier

Notre modèle métier ne change pas, vous pouvez le retrouver dans l’article d’introduction à RestKit.

Le point d’entrée d’une application iOS est une classe qui étend la super classe UIRespsonder et qui implémente le protocole UIApplicationDelegate.

Nous allons donc créer une classe appelée AppDelegate. Son interface sera la suivante:

#import <uikit /UIKit.h>

@interface AppDelegate : UIResponder <uiapplicationdelegate>

@property (strong, nonatomic) UIWindow *window;

@end

Le point d’entrée réel de l’application est la méthode application:didFinishLaunchingWithOptions: du protocole UIApplicationDelegate. C’est ici que nous ajouterons le code d’initialisation de RestKit:

#import "AppDelegate.h"
#import <restkit /RestKit.h>
#import "NSDateFormatter+XBAdditions.h"
#import "GHRepository.h"
#import "GHUser.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [self initializeLoggers];
    [self initializeRestKit];

    return YES;
}

- (void)initializeLoggers {
    RKLogConfigureByName("RestKit/*", RKLogLevelInfo);
}

-(void)initializeRestKit {
    [self initializeObjectManager];
    [self initializeObjectMapping];
}

- (void)initializeObjectManager {
    RKObjectManager *objectManager = [RKObjectManager managerWithBaseURLString:@"https://api.github.com/"];
    objectManager.client.cachePolicy = RKRequestCachePolicyDefault;
    // RKRequestCachePolicyDefault = RKRequestCachePolicyEtag | RKRequestCachePolicyTimeout
}

- (void)initializeObjectMapping {

    // 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;

    RKObjectMapping *repositoryObjectMapping = [GHRepository mapping];
    [omp addObjectMapping:repositoryObjectMapping];
    [omp setObjectMapping:repositoryObjectMapping forResourcePathPattern:@"/orgs/:organization/repos"];

    RKObjectMapping *userObjectMapping = [GHUser mapping];
    [omp addObjectMapping:userObjectMapping];
    [omp setObjectMapping:userObjectMapping forResourcePathPattern:@"/orgs/:organization/public_members"];
}

@end

Dans la méthode initializeObjectMapping, nous avons déclaré un pattern de chemin de ressource complémentaire: "/orgs/:organization/public_members", qui permet de mapper la liste des utilisateurs sur la classe GHUser du modèle métier.

Création d’un StoryBoard

Depuis iOS 5.0 Apple propose dans Xcode la notion de storyboard. Plutôt que de manipuler isolément des fichiers XIB (Morceaux d’interface graphique), Apple a tenté de fournir un ensemble plus consistant et visuel permettant de manipuler l’ensemble des vues et leurs interactions. Par exemple, le storyboard permet d’accéder à une cartographie des vues applicatives, ou encore des marqueurs visuels représentant les liaisons entre ces vues. Le résultat est plutôt convainquant bien qu’un peu difficile à manipuler si vous n’êtes pas équipé d’un grand écran haute densité.

Dans le contexte de l’application développée, 2 vues de type UITableView seront proposées, l’une listant les utilisateurs d’une organisation Github, l’autre listant les repositories de cette même organisation. Les vues seront accessibles grâce à une UITabBar.

Sur le storyboard, vous pouvez observer deux vues de type Navigation Controller. Ces deux vues ont pour rôle de gérer la navigation dans l’application. Par défaut, elles sont matérialisées par une barre en haut de l’écran qui affiche le titre, un bouton retour sur la gauche permettant de revenir à l’écran précédent, et d’autres actions.

Pour créer un storyboard semblable à celui présenté, vous devez dans un premier temps insérer un TabBar Controller comme suit:

Pour cela, après avoir cliqué sur votre storyboard, vous pouvez faire glisser un objet TabBar Controller depuis le navigateur d’objet de base à droite d’Xcode:

Puis vous devez insérer deux Table View Controllers:

Les deux controllers créés avec le tab bar controller sont inutiles, vous pouvez les supprimer:

Vous devez maintenant relier les Navigation Controller associés aux TableView Controller avec le TabBar Controller. Pour cela vous réalisez un clic droit sur le TabBar Controller, l’écran suivant apparaitra:

Vous cliquerez et tirerez le curseur depuis le symbole plus de la ligne qui indique view controller, jusqu’à chacun des deux Navigation Controller:

Vous obtiendrez le résultat suivant:

Pour chacune des vues, vous devrez affecter un StoryBoard Id qui servira à identifier la vue de façon unique:

Vous pourrez configurer le style de la barre de navigation de votre TableView Controller depuis la combo Top Bar:

Vous pourrez configurer le titre du TableView Controller via l’inspecteur d’attribut en cliquant sur sa barre de navigation:

Vous pourrez alors configurer le controller de chacun des TableView Controller:

Si vous souhaitez customiser les icônes et titres des items du TabBar Controller, vous pourrez le faire depuis l’inspecteur d’attributs comme suit:

Pour terminer, vous devrez marquer le TabBar Controller comme étant la vue initiale du StoryBoard, c’est à dire la vue de démarrage. Pour cela, vous devrez cocher la case Is Initial View Controller de l’inspecteur d’attributs:

Les controller applicatifs

Une fois le StoryBoard configuré, nous pouvons maintenant nous concentrer sur les controller applicatifs. Ceux dédiés à la gestion des vues listant respectivement les utilisateurs et les repositories sont très similaires, nous étudierons donc uniquement l’UITableViewController de la vue listant les repositories.

Nous pourrions choisir de travailler bas niveau et implémenter complètement notre controller, cependant RestKit fournit une classe qui pré-mâche une bonne partie du travail. La classe fournie par RestKit fonctionne par délégation, nous avons donc toujours à étendre la classe UITableView dans l’interface du controller:

#import <uikit /UIKit.h>

@interface GHRepositoryTableViewController : UITableViewController<rktablecontrollerdelegate>

@end

Le code de l’interface est plutôt succinct, cependant, il nous renseigne sur le fait que notre implémentation à besoin de mettre en oeuvre le protocole RKTableControllerDelegate qui permet de réagir à différents événements générés par RestKit.

Dans notre implémentation, nous allons commencer par déclarer une classe d’extension. Cela permet de déclarer des propriétés qui ne sont accessibles que par l’implémentation de la classe. Une catégorie ne pourra pas accéder, par exemple, aux données de la classe d’extension:

@interface GHRepositoryTableViewController ()
@property (nonatomic, strong) RKTableController *tableController;
@property (nonatomic, strong) UIImage* defaultAvatarImage;
@end

Une classe d’extension est définie par le nom de la classe qu’elle étend ainsi que des parenthèses vides.
Les propriétés tableController et defaultAvatarImage correspondent à des propriétés privées de l’implémentation du controller.
La propriété tableController représente le controller RestKit auquel nous allons déléguer le traitement de notre classe.

Evénements d’initialisation du controller

#import <restkit /RestKit.h>
#import </restkit><restkit /UI.h>
#import "GHRepository.h"
#import "GHRepositoryTableViewController.h"
#import "SDImageCache.h"
#import "SDWebImageManager.h"
#import "GHRepositoryCell.h"
#import "UIImageView+WebCache.h"
#import "UIColor+XBAdditions.h"
#import "UIScreen+XBAdditions.h"

@interface GHRepositoryTableViewController ()
...
@end

@implementation GHRepositoryTableViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self configure];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.tableController loadTableFromResourcePath:@"/orgs/xebia-france/repos"];
}

...

@end

Dans un premier temps, sur l’événement viewDidLoad:, nous allons configurer notre controller. Cette configuration est déléguée à différentes méthodes que nous allons voir par la suite.
Puis, sur l’événement viewWillAppear:, nous allons initialiser la demande de chargement de la la table avec les données JSON renvoyées par la ressource de l’API GitHub. Nous le faisons en appelant la méthode loadTableFromResourcePath du controller RestKit tableController que nous avons au préalable initialisé sur l’événement viewDidLoad.

@implementation GHRepositoryTableViewController

...

- (void)configure {
    self.defaultAvatarImage = [UIImage imageNamed:@"github-gravatar-placeholder"];
    [self configureTableViews];
    [self configureTableController];
}

...

@end

Dans la méthode configure, nous configurons les views gérées par le controller, puis le controller en lui-même.

Configuration des vues

Les vues à initialiser sont la table en elle-même, ainsi que ses sous-vues, en l’occurrence ici, la vue gérant le Pull to refresh:

- (void)configureTableViews {
    [self configureTableView];
    [self configurePullToRefreshTriggerView];
}

- (void)configureTableView {
    self.tableView.backgroundColor = [UIColor colorWithPatternImageName:@"bg_home_pattern"];
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;

    [self.tableView registerNib:[UINib nibWithNibName:@"GHRepositoryCell" bundle:nil] forCellReuseIdentifier:@"GHRepository"];
}

- (void)configurePullToRefreshTriggerView {
    NSBundle *restKitResources = [NSBundle restKitResourcesBundle];
    UIImage *arrowImage = [restKitResources imageWithContentsOfResource:@"blueArrow" withExtension:@"png"];
    [[RKRefreshTriggerView appearance] setTitleFont:[UIFont fontWithName:@"HelveticaNeue-Bold" size:13]];
    [[RKRefreshTriggerView appearance] setLastUpdatedFont:[UIFont fontWithName:@"HelveticaNeue" size:11]];
    [[RKRefreshTriggerView appearance] setArrowImage:arrowImage];
}

La configuration des vues permet principalement de définir les aspects cosmétiques de celles-ci. Néanmoins, la vue de la table est ici associée avec le Nib qui sera utilisé pour customiser le rendu des cellules:

[self.tableView registerNib:[UINib nibWithNibName:@"GHRepositoryCell" bundle:nil] forCellReuseIdentifier:@"GHRepository"];

Cette ligne de code indique que le Nib (Correspondant au fichier du même nom et portant l’extension .xib) sera utilisé pour effectuer le rendu des cellules ayant l’identifiant GHRepository.

Configuration du controller

- (void)configureTableController {
    self.tableController = [[RKObjectManager sharedManager] tableControllerForTableViewController:self];

    self.tableController.delegate = self;

    self.tableController.pullToRefreshEnabled = YES;
    self.tableController.variableHeightRows = YES;

    self.tableController.imageForOffline = [UIImage imageNamed:@"offline.png"];
    self.tableController.imageForError = [UIImage imageNamed:@"error.png"];
    self.tableController.imageForEmpty = [UIImage imageNamed:@"empty.png"];

    [self.tableController mapObjectsWithClass:[GHRepository class] toTableCellsWithMapping:[self createCellMapping]];
}

- (RKTableViewCellMapping *)createCellMapping {
    RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping];
    cellMapping.cellClassName = @"GHRepositoryCell";
    cellMapping.reuseIdentifier = @"GHRepository";

    cellMapping.heightOfCellForObjectAtIndexPath = ^ CGFloat(GHRepository *repository, NSIndexPath* indexPath) {
        ...
        return height;
    };

    [cellMapping mapKeyPath:@"name" toAttribute:@"titleLabel.text"];
    [cellMapping mapKeyPath:@"description_" toAttribute:@"descriptionLabel.text"];

    return cellMapping;
}

C’est dans la méthode configureController que nous déclarons le controller RestKit. Celui-ci est initialisé via l’instance partagée de la classe RKObjectManager. Nous déclarons le controller applicatif comme délégué du controller RestKit, ce qui permet à celui-ci de déléguer le traitement de certains événements au controller applicatif.
La méthode configureController permet également de configurer l’activation du mécanisme de Pull to refresh, ainsi que les images associées à différents états de la vue en cas d’erreur ou autre. Nous indiquons également que les cellules de la table auront une hauteur variable, qui sera calculée ou bien définie sur les mapping de cellule que nous verrons plus tard.
Enfin, la méthode configureController définit les mapping à utiliser pour associer les objets de la collection affichée par le controller avec les éléments de la vue. Les cellules sont mappées selon une configuration de type RKTableViewCellMapping. Cette classe définit non seulement les mapping entre les objets de la collection et les éléments de la cellule, mais également le nom de la classe qui sera utilisée pour instancier la cellule et l’identifiant de réutilisation de la cellule.

Calcul de la hauteur de la cellule

La méthode heightOfCellForObjectAtIndexPath implémentée lors de la configuration des mapping de cellule permet de calculer la hauteur d’une ligne. En l’occurrence, la méthode permet de calculer la hauteur de la description compte tenu de contraintes et informations fournies telles que la largeur maximale allouée, la taille de la police utilisée ou bien encore la hauteur prise par les autres éléments de la cellule.

...
#define FONT_SIZE 13.0f
#define CELL_BORDER_WIDTH 88.0f // 320.0f - 232.0f
#define CELL_MIN_HEIGHT 64.0f
#define CELL_BASE_HEIGHT 48.0f
#define CELL_MAX_HEIGHT 1000.0f
...
@implementation GHRepositoryTableViewController
...
- (RKTableViewCellMapping *)createCellMapping {
    RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping];
    ...
    cellMapping.heightOfCellForObjectAtIndexPath = ^ CGFloat(GHRepository *repository, NSIndexPath* indexPath) {
        CGRect bounds = [UIScreen getScreenBoundsForCurrentOrientation];
        CGSize constraint = CGSizeMake(bounds.size.width - CELL_BORDER_WIDTH, CELL_MAX_HEIGHT);
        CGSize size = [repository.description_ sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE]
                                          constrainedToSize:constraint
                                              lineBreakMode:UILineBreakModeWordWrap];
        CGFloat height = MAX(CELL_BASE_HEIGHT + size.height, CELL_MIN_HEIGHT);

        return height;
    };
    ...
    return cellMapping;
}
...
@end

Configuration du rendu des cellules

Nous n’avons pas encore terminé, puisque nous devons maintenant configurer le rendu des cellules. Lors de la configuration du controller, nous avons défini des mapping de cellule qui s’appliquent sur un objet représentant la cellule, dont le type est GHRepositoryCell. Nous devons donc créer la classe correspondant au type GHRepositoryCell.

L’interface sera la suivante:

#import <uikit /UIKit.h>
#import "GHRepositoryCell.h"

@interface GHRepositoryCell : UITableViewCell

@property (nonatomic, strong) IBOutlet UILabel *titleLabel;
@property (nonatomic, strong) IBOutlet UILabel *descriptionLabel;

@end

et l’implémentation :

#import "GHRepositoryCell.h"
#import "QuartzCore/QuartzCore.h"

@implementation GHRepositoryCell

- (void)layoutSubviews {
    [super layoutSubviews];
    self.imageView.frame = CGRectMake(10,9,44,44);
    self.imageView.layer.masksToBounds = YES;
    self.imageView.layer.cornerRadius = 3.0;
}

@end

Nous devons également créer le Nib / Xib correspondant de la cellule ayant le rendu souhaité via l’assistant de création de fichier:

Une fois le fichier *.xib créé, un objet Table View Cell doit être ajouté:

Sa taille doit être configurée avec une largeur de 320 pixels et une hauteur de 44 pixels:

Un label correspondant au titre doit être ajouté. Il sera configuré pour s’adapter à la largeur de l’écran, aura pour coordonnées: [66, 5, 232, 16], une taille de police 13, ainsi qu’un style de retour de ligne de type Word Wrap.

Un label correspondant à la description doit également être ajouté. Il sera configuré pour s’adapter à la largeur de l’écran ainsi qu’à la hauteur de la cellule. Il aura pour coordonnées: [66, 20, 232, 18], une taille de police 13, ainsi qu’un style de retour de ligne de type Word Wrap.

Il faudra ensuite créer les liens entre les IBOutlet de la classe représentant la cellule et les éléments graphiques correspondants du fichier Xib. Pour cela il faut passer l’éditeur en mode splitté avec le code source d’un côté et le fichier Xib de l’autre. Faire un clic droit sur l’encoche dans la goutière sur les lignes correspondant aux définitions de variables typées IBOutlet et tirer un lien vers l’élément graphique du Xib:

Ajout d’un avatar aux cellules

L’ajout d’avatars aux cellules du table view des utilisateurs peut être réalisé facilement grâce à l’extrait de code suivant provenant de la classe GHUserTableViewController:

- (void)tableController:(RKAbstractTableController *)tableController willDisplayCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath {
 GHUser *user = object;
 GHUserCell *userCell = (GHUserCell *)cell;
 [userCell.imageView setImageWithURL:[user avatarImageUrl] placeholderImage:self.defaultAvatarImage];
}

Un avatar par défaut doit être chargé dans la méthode viewDidLoad: dans le cas où l’avatar de l’utilisateur ne pourrait être récupéré:

self.defaultAvatarImage = [UIImage imageNamed:@"github-gravatar-placeholder"];

Conclusion

Nous avons vu à travers cet article comment utiliser RestKit dans une application iOS pour récupérer une collection de données et l’afficher dans une vue de type UITableView. RestKit nous a permis de simplifier l’implémentation tout en réduisant fortement le nombre de lignes de code à produire.

Code source

Le code source de l’article est disponible sur le repository GitHub restkit-tutorial-ios.

Articles Liés