UITableView + API GitHub + CocoaPods = Keep it Simple, keep it Stupid!

UITableView + API GitHub + CocoaPods = Keep it Simple, keep it Stupid!

Nous avons vu dans un article précédent comment utiliser RestKit pour se faciliter la vie lorsque nous travaillons avec des services web exploitant le format JSON. Toutefois, l’utilisation d’un framework fullstack peut se révéler contraignant dès lors que l’on sort du cadre pour lequel il a été destiné. ...

CocoaPods

Nous avons vu dans un article précédent comment utiliser RestKit pour se faciliter la vie lorsque nous travaillons avec des services web exploitant le format JSON. Toutefois, l’utilisation d’un framework fullstack peut se révéler contraignant dès lors que l’on sort du cadre pour lequel il a été destiné. Malheureusement, cela arrive souvent assez rapidement.

Nous verrons dans cet article comment reproduire une application similaire à celle construite grâce à RestKit, sans pour autant abandonner l’idée de gagner du temps grâce à l’utilisation de quelques librairies bien choisies :

Le monde iOS, comme tout environnement mature, dispose de nombreuses librairies Open Source intégrables dans vos projets via l’utilisation d’un Dependency Manager. Dans notre cas, nous utiliserons CocoaPods déjà présenté dans un article précédent.

La création de ce tutoriel fait suite à une présentation en XKE d’une session d’1h30 d’introduction aux développements iOS. Ce tutoriel est donc réalisable en un temps raisonnable en suivant les instructions ci-dessous.

Création du projet

Avant d’utiliser CocoaPods, vous devrez créer un projet de type Empty Application pour iOS via XCode:

Vous aurez ensuite à créer un fichier Podfile à la racine de votre projet XCode avec les dépendances suivantes:

platform :ios, '5.0'
pod 'JSONKit', '1.5pre'
pod 'Underscore.m', '0.2.0'
pod 'AFNetworking', '1.1'
pod 'MBProgressHUD', '0.6'
pod 'SVPullToRefresh', '0.4'
pod 'SDURLCache', '1.2'
pod 'AFHTTPRequestOperationLogger', '0.10.0'
pod 'DCKeyValueObjectMapping', '1.3'

Puis, vous devrez exécuter la commande suivante pour générer le workspace et intégrer le code des dépendances:

pod install

Il faudra alors relancer votre projet en ouvrant le fichier *.XCodeWorkspace, plutôt que le fichier *.XCodeProject. Vous serez alors prêt à développer votre application.

Création du StoryBoard

Nous avons choisi de créer une application vide. Il nous faut donc créer un StoryBoard qui contiendra les différentes vues que nous aurons à créer. Pour cela, nous allons nous  rendre dans le menu: File > New > File… et sélectionner StoryBoard dans la section iOS > User Interface. Vous pourrez, par exemple, nommer le fichier: UITableViewTutorial.storyboard

Une fois le storyboard créé, il est nécessaire de l’associer en tant que storyboard par défaut afin qu’il soit utilisé au lancement de l’application. Cela peut être réalisé via l’onglet de configuration Target Summary de la target par défaut, ici UITableViewTutorial.

Création d’un UITableViewController

Nous souhaitons afficher un listing de données, un table view controller correspond donc à notre besoin. Nous devons créer un table view controller dans le storyboard de l’application et l’assigner comme vue de démarrage de l’application. Afin de faciliter la navigation, nous souhaitons également créer un navigation controller.

XCode facilite la création de ces deux view controllers en faisant glisser depuis l’explorateur d’objets un élément de type: Navigation Controller sur le storyboard. Cela aura pour effet de créer automatiquement le navigation controller, le table view controller ainsi que la liaison de navigation entre les deux view controllers.

Lorsqu’un projet a été créé sans storyboard avec un template de type Empty Application, il est nécessaire de modifier le code d’initialisation de l’application et de supprimer le code inutile généré. La méthode de démarrage de l’application application:didFinishLaunchingWithOptions: dans la classe AppDelegate doit retourner tout simplement la valeur YES:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
 return YES;
}

Coloration des barres de navigation

La coloration des barres de navigation peut-être customisée via l’édition des propriétés Status Bar et Top Bar de l’onglet Simulated metrics du navigation controller. Nous choisirons la couleur noir, plus passe-partout que le bleu de l’interface iPhone un peu vieillissant.

Titre de la table view

Le titre de la table view peut être changé via la propriété Title de l’onglet Navigation Item. Nous l’appellerons Repositories.

Style des cellules

Le style de la cellule sera de type Subtitle et correspond donc à un type standard qui peut être configuré via propriété Style de l’onglet Table View Cell du prototype de cellule du tableau.

Identifiant de réutilisation

Afin d’optimiser la performance de ses table views, iOS permet la réutilisation de ses cellules. Pour en bénéficier, un identifiant de cellule réutilisable doit être défini. Nous utiliserons l’identifiant RepositoryCell, configurable via la propriété Identifier de l’onglet T_able View Cell_.

Association de la table view avec son controller

Nous n’avons pas encore créé de table view controller, nous allons donc le créer via le menu File > New > File … et choisir Objective-C class dans la section iOS > Cocoa Touch. Le controller s’appellera GHRepositoryTableViewController.

Une fois la classe créée, nous pouvons associer la table view avec son controller via la propriété Class de l’onglet Custom Class de la table view. Il doit correspondre au nom de la classe du controller gérant la vue.

Récupération des données

Flux de données

Le service web utilisé sera toujours le service de listing des répositories GitHub d’une organisation donnée.

Le flux de données correspondant à la liste des repositories de l’organisation FaceBook correspond à la structure suivante:

[
 {
    "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

La modélisation métier proposée dans les tutoriels précédents sera reprise. Nous avons donc une interface et une implémentation correspondant aux classes GHOwner et GHRepository à ajouter.

Définition de la classe GHOwner

Interface

@interface GHOwner : NSObject
@property (nonatomic, strong) NSString *gravatar_id;

@property (nonatomic, strong, readonly) NSURL *avatarImageUrl;

@end

Implémentation

#import "GHOwner.h"
@implementation GHOwner

- (NSURL *)avatarImageUrl {
    NSString *gravatarUrlStr = [NSString stringWithFormat:@"https://secure.gravatar.com/avatar/%@?s=44&d=404", self.gravatar_id];
    return [NSURL URLWithString: gravatarUrlStr];
}

@end

Définition de la classe GHRepository

Interface

#import "GHOwner.h"
@interface GHRepository : NSObject

@property (nonatomic, strong) NSNumber *forks;
@property (nonatomic, strong) NSString *language;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSNumber *open_issues;
@property (nonatomic, strong) GHOwner *owner;
@property (nonatomic, strong) NSString *url;
@property (nonatomic, strong) NSNumber *watchers;
@property (nonatomic, strong, readonly) NSString *description_;

@end

Implémentation

#import "GHRepository.h"
@implementation GHRepository

- (NSString *)description_ {
    return [NSString stringWithFormat:@"%@ - %@ Forks - %@ Watchers - %@ Issues", self.language ? self.language : @"No Language", self.forks, self.watchers, self.open_issues];
}

@end

Chargment des données avec AFNetworking

AFNetworking est une librairie HTTP très appréciée pour sa simplicité d’utilisation et son efficacité via l’utilisation des Operation Queues. Nous l’utiliserons pour nous faciliter le travail d’accès aux données du service via HTTP.

Dans la méthode viewDidLoad: du table view controller, nous appellerons une méthode qui aura la responsabilité de charger les données et déclencher le rechargement du composant. Nous appellerons cette méthode loadDataWithCallback.

- (void)loadDataWithCallback:(void(^)())callback {
    AFHTTPClient *httpClient = [AFHTTPClient clientWithBaseURL:[NSURL URLWithString:@"https://api.github.com"]];
    NSURLRequest *urlRequest = [httpClient requestWithMethod:@"GET" path:@"/orgs/facebook/repos" parameters:nil];
    [self showProgressHUDWithMessage:NSLocalizedString(@"Chargement des données", nil) graceTime:0.5];
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:urlRequest];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
  ...

        [self.tableView reloadData];
        [self dismissProgressHUD];
  if (callback) {
            callback();
        }
    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        XBLogWarn(@"Error: %@", error);
        self.dataSource = [NSArray array];
        [self.tableView reloadData];
        [self showErrorProgressHUDWithMessage:NSLocalizedString(@"Erreur de chargement des données !", nil) afterDelay:0.5];
        if (callback) {
            callback();
        }
    }];
    [operation start];
}

Il est nécessaire d’appeler cette méthode au chargement de la vue, dans la méthode viewDidLoad si nous souhaitons que les données soient chargées automatiquement lors du premier affichage de la vue:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self initProgressHUD];
    [self loadDataWithCallback:^{}];
}

Logging des requêtes / réponses HTTP

Afin de faciliter le travail de déboggage via le traçage des appels HTTP, la micro extension AFHTTPRequestOperationLogger de la librairie AFNetworking a été ajoutée. Elle permet d’afficher les informations relatives aux requêtes effectuées via la librairie.

- (void)setupNetworkingLogger {
    [[AFHTTPRequestOperationLogger sharedLogger] startLogging];
    [[AFHTTPRequestOperationLogger sharedLogger] setLevel:AFLoggerLevelDebug];
}

Cette méthode doit être appelée au démarrage de l’application et son initialisation prend donc place dans la classe AppDelegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [self setupNetworkingLogger];
    return YES;
}

Ajout d’indicateurs de chargement

Nous souhaitons ajouter un indicateur de chargement qui apparaît lors du requêtage des données, et qui disparaît une fois les données chargées. Un message d’erreur doit être affiché lorsque le chargement des données termine en erreur. Nous utiliserons la librairie MBProgressHUD, qui propose une approche intéressante : l’indicateur de chargement n’apparait que si le temps d’execution de l’opération devient “long”. Si le chargement est suffisamment rapide, l’indicateur n’apparaît pas, et contribue à la fluidité de l’expérience utilisateur

Dans un premier temps, le code suivant doit être ajouté au table view controller:

#pragma mark - Progress HUD

- (void)initProgressHUD {
 self.progressHUD = [[MBProgressHUD alloc] initWithView:self.navigationController.view];
}

- (void)showProgressHUDWithMessage:(NSString *)message graceTime:(float)graceTime {
 self.progressHUD.mode = MBProgressHUDModeIndeterminate;
 self.progressHUD.labelText = NSLocalizedString(message, message);
 self.progressHUD.graceTime = graceTime;
 self.progressHUD.taskInProgress = YES;
 [self.navigationController.view addSubview:self.progressHUD];
 [self.progressHUD show:YES];
}

- (void)showErrorProgressHUDWithMessage:(NSString *)errorMessage afterDelay:(float)delay {
 self.progressHUD.mode = MBProgressHUDModeText;
 self.progressHUD.labelText = errorMessage;
 [self.progressHUD hide:YES afterDelay:delay];
 self.progressHUD.taskInProgress = NO;
 [self.navigationController.view addSubview:self.progressHUD];
}

- (void)dismissProgressHUD {
 self.progressHUD.taskInProgress = NO;
 [self.progressHUD hide:YES];
}

Une variable d’instance doit également être ajoutée dans la déclaration de l’interface privée du table view controller:

@interface GHRepositoryTableViewController ()
@property (nonatomic, strong) MBProgressHUD *progressHUD;
@end

Puis le composant MBProgressHUD doit être initialisé dans la méthode viewDidLoad du controller.

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

Mapping du JSON avec le modèle de données

Pour convertir les données reçues au format JSON, nous utiliserons 2 librairies. La première, JSONKit, est reconnue pour sa stabilité et ses performances. Elle permet de convertir une structure de données JSON vers une représentation de données au format NSDictionary / NSArray. Dans un second temps ce format intermédiaire pourra être converti vers notre modèle de données grâce à la librairie DCKeyValueObjectMapping.

- (void)buildDataSourceFromResponseData:(id)responseObject {
    NSString *jsonString = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
    NSArray *json = [jsonString objectFromJSONString];
    DCParserConfiguration *parserConfiguration = [DCParserConfiguration configuration];
    parserConfiguration.datePattern = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
    DCKeyValueObjectMapping *parser = [DCKeyValueObjectMapping mapperForClass: GHRepository.class andConfiguration:parserConfiguration];
    self.dataSource = [parser parseArray:json];
}

Le code de chargement devient donc:

- (void)loadDataWithCallback:(void(^)())callback {
    AFHTTPClient *httpClient = [AFHTTPClient clientWithBaseURL:[NSURL URLWithString:@"https://api.github.com"]];
    NSURLRequest *urlRequest = [httpClient requestWithMethod:@"GET" path:@"/orgs/facebook/repos" parameters:nil];
    [self showProgressHUDWithMessage:NSLocalizedString(@"Chargement des données", nil) graceTime:0.5];

    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:urlRequest];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {

        [self buildDataSourceFromResponseData:responseObject];

        [self.tableView reloadData];
        [self dismissProgressHUD];
        if (callback) {
            callback();
        }
    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        XBLogWarn(@"Error: %@", error);
        self.dataSource = [NSArray array];
        [self.tableView reloadData];
        [self showErrorProgressHUDWithMessage:NSLocalizedString(@"Erreur de chargement des données !", nil) afterDelay:0.5];
        if (callback) {
            callback();
        }
    }];
    [operation start];
}

Il faut également ajouter une property dans l’interface privée de la classe pour stocker le table de résultats. Nous l’appellerons dataSource:

@interface GHRepositoryTableViewController ()
@property (nonatomic, strong) MBProgressHUD *progressHUD;
@property (nonatomic, strong) NSArray *dataSource;
@end

Support du caching de données

La librairie SDURLCache sera utilisée pour fournir un support de cache fichier stockant les réponses HTTP. La librairie s’appuie sur les en-têtes HTTP et le code de retour pour décider si les données de réponse doivent être mises en cache ou non. Cela permet une consultation des données offline si aucune connectivité est disponible lors d’un requêtage ultérieur. Le code d’initialisation est à intégrer à la classe AppDelegate.

- (void)setupUrlCache {
    SDURLCache *URLCache = [[SDURLCache alloc] initWithMemoryCapacity: 1024*1024*2 diskCapacity: 1024*1024*20  diskPath: [SDURLCache defaultCachePath]];
    [NSURLCache setSharedURLCache:URLCache];
}

Il faut également modifier la méthode application:didFinishLaunchingWithOptions: pour appeler la méthode ajoutée:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [self setupUrlCache];
    [self setupNetworkingLogger];
    return YES;
}

Configuration du table view controller

Plusieurs méthodes issues du protocole UITableViewDataSource sont à renseigner pour finaliser l’implémentation initiale du table view controller.

numberOfSectionsInTableView:

Cette méthode permet de renseigner le nombre de sections dans la table view. Ici nous n’avons besoin que d’une seule section qui est la section par défaut. Nous pouvons donc retourner la valeur: 1.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
 return 1;
}

tableView:numberOfRowsInSection:

Cette méthode permet d’indiquer combien nous avons de lignes par section. Ici, nous en avons autant qu’il y a de résultats dans les données chargées. Cette valeur correspond donc au nombre de résultats présents dans la variable dataSource:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
 return self.dataSource.count;
}

tableView:cellForRowAtIndexPath:

Cette méthode permet de configurer la cellule correspondant à l’index fourni en paramètre. Nous utilisons ici la notion de reuseIdentifier pour recycler les cellules ayant déjà servi (dans un souci de performance). Il est important de noter que le reuseIdentifier doit être une variable statique, sans quoi le mécanisme de réutilisation de cellule ne fonctionne pas correctement.

En titre de cellule, nous choisirons d’afficher le nom du repository. En detail, nous utiliserons la propriété description_ qui regroupe différentes informations importantes à propos du repository.

Nous choisirons également d’afficher une image sur chaque cellule. Cette image n’est autre que le gravatar de l’auteur du repository. L’identifiant du gravatar est fourni par l’API, tout comme son URL. Cependant, l’URL ne convenant pas à notre besoin, nous décidons de la reconstruire en indiquant que nous souhaitons une image d’une taille de 44 pixels, ainsi qu’une erreur 404 lorsque l’image n’est pas trouvée. Ce dernier point est important car nous voulons pouvoir afficher une image placeholder dans le cas où une image n’a pas encore été chargée (lorsqu’elle n’existe pas ou bien lorsqu’il n’est pas possible de la récupérer faute de connectivité).

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"RepositoryCell";
    UITableViewCell *repositoryCell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    GHRepository *repository = self.dataSource[(NSUInteger) indexPath.row];
    repositoryCell.textLabel.text = repository.name;
    repositoryCell.detailTextLabel.text = repository.description_;
    [repositoryCell.imageView setImageWithURL:repository.owner.avatarImageUrl placeholderImage:self.defaultAvatarImage];
    return repositoryCell;
}

A noter que la méthode setImageWithURL:placeholderImage: est fournie par la librairie AFNetworking et ne fait pas partie de l’API de base iOS.

L’avatar par défaut doit être initialisé au chargement de la vue, il faut donc l’ajouter à la méthode viewDidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.defaultAvatarImage = [UIImage imageNamed:@"github"];
    [self initProgressHUD];
    [self loadDataWithCallback:^{}];
}

Nous devons également ajouter une property dans l’interface privée de la classe:

@interface GHRepositoryTableViewController ()
@property (nonatomic, strong) MBProgressHUD *progressHUD;
@property (nonatomic, strong) NSArray *dataSource;
@property (nonatomic, strong) UIImage *defaultAvatarImage;
@end

Fonctionnalités supplémentaires

Ajout du Pull To Refresh

Une fonctionnalité intéressante à ajouter dans les views de type table view est le pull to refresh. La librairie SVPullToRefresh permet de l’ajouter sans être invasive, pour cela il suffit d’implémenter une méthode dans le table view controller:

- (void)setupPullToRefresh {
    __weak typeof(self) weakSelf = self;
    [self.tableView addPullToRefreshWithActionHandler:^{
        [weakSelf loadDataWithCallback:^{
            [weakSelf.tableView.pullToRefreshView stopAnimating];
            [weakSelf.tableView reloadData];
        }];
    }];
}

Il est également nécessaire d’appeler cette méthode au chargement de la vue, dans la méthode viewDidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.defaultAvatarImage = [UIImage imageNamed:@"github"];
    [self setupPullToRefresh];
    [self initProgressHUD];
    [self loadDataWithCallback:^{}];
}

Ajout de l’Infinite Scrolling

La fonction d’infinite scrolling fait partie des comportements naturels que l’on s’attend à trouver sur une table view lorsqu’on souhaite afficher des données paginées. Lorsque l’on atteint en scrollant la fin des premiers résultats chargés, les résultats suivants sont chargés automatiquement, permettant de poursuivre le scroll. Encore une fois, la librairie SVPullToRefresh nous vient en secours puisqu’elle en propose une implémentation clean et surtout non invasive.

Tout comme pour l’ajout de la fonction de Pull to Refresh, il est nécessaire d’ajouter un code d’initialisation. Pour cela, nous allons ajouter une méthode setupInfiniteScroll:

- (void)setupInfiniteScroll {
    __weak typeof(self) weakSelf = self;
    [self.tableView addInfiniteScrollingWithActionHandler:^{
        if (weakSelf.nextPageURL) {
            [weakSelf loadDataWithCallback:^{
                [weakSelf.tableView.infiniteScrollingView stopAnimating];
                [weakSelf.tableView reloadData];
            }];
        }
        else {
            [weakSelf.tableView.infiniteScrollingView stopAnimating];
        }
    }];
}

Et modifier encore une fois la méthode viewDidLoad: de notre table view controller pour ajouter son initialisation:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.defaultAvatarImage = [UIImage imageNamed:@"github"];
    [self setupPullToRefresh];
    [self setupInfiniteScroll];
    [self initProgressHUD];
    [self loadDataWithCallback:^{}];
}

Nous devons également ajouter une propriété permettant de stocker l’URL de la page suivante si l’information nous a été renvoyée par les en-têtes:

@interface GHRepositoryTableViewController ()
@property (nonatomic, strong) MBProgressHUD *progressHUD;
@property (nonatomic, strong) NSMutableArray *dataSource;
@property (nonatomic, strong) UIImage *defaultAvatarImage;
@property (nonatomic, strong) NSString *nextPageURL;
@end

La méthode tryFindNextPageUrl: est ajoutée au table view controller afin de permettre d’extraire le lien vers la page de données suivante:

- (void)tryFindNextPageUrl:(NSHTTPURLResponse *)response {
    NSString *linkHeader = response.allHeaderFields[@"Link"];
    if (linkHeader) {
        NSArray *links = [linkHeader componentsSeparatedByString:@","];

        NSString * __block nextPageURL = nil;
        [links enumerateObjectsUsingBlock:^(NSString *link, NSUInteger idx, BOOL *stop) {
            NSArray *linkParts = [link componentsSeparatedByString:@";"];
            NSString *rel = [linkParts[1] componentsSeparatedByString:@"""][1];
            if ([rel isEqualToString:@"next"]) {
                nextPageURL = [linkParts[0] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
                *stop = YES;
            }
        }];
        self.nextPageURL = [nextPageURL substringFromIndex:@"https://api.github.com".length];
    }
    else {
        self.nextPageURL = nil;
    }
}

Cette méthode est appelée depuis la méthode loadDataWithCallback: qui est modifiée comme suit:

- (void)loadDataWithCallback:(void(^)())callback {
    AFHTTPClient *httpClient = [AFHTTPClient clientWithBaseURL:[NSURL URLWithString:@"https://api.github.com"]];
    NSString *path = self.nextPageURL ? self.nextPageURL : @"/orgs/facebook/repos";
    NSURLRequest *urlRequest = [httpClient requestWithMethod:@"GET" path:path parameters:nil];
    [self showProgressHUDWithMessage:NSLocalizedString(@"Chargement des données", nil) graceTime:0.5];
    XBLogInfo(@"[GET] Requesting JSON payload at: %@", urlRequest.URL);
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:urlRequest];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        [self tryFindNextPageUrl: operation.response];
        [self buildDataSourceFromResponseData:responseObject];
        [self.tableView reloadData];
        [self dismissProgressHUD];
        if (callback) {
            callback();
        }
    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        XBLogWarn(@"Error: %@", error);
        self.dataSource = [NSMutableArray array];
        [self.tableView reloadData];
        [self showErrorProgressHUDWithMessage:NSLocalizedString(@"Erreur de chargement des données !", nil) afterDelay:0.5];
        if (callback) {
            callback();
        }
    }];
    [operation start];
}

La méthode d’initialisation du Pull To Refresh doit également être modifiée pour réinitialiser le dataSource et l’URL de page suivante afin d’assurer un bon fonctionnement lors d’un rechargement des données de la première page de résultat:

- (void)setupPullToRefresh {
    __weak typeof(self) weakSelf = self;
    [self.tableView addPullToRefreshWithActionHandler:^{
        weakSelf.nextPageURL = nil;
        weakSelf.dataSource = nil;
        [weakSelf loadDataWithCallback:^{
            [weakSelf.tableView.pullToRefreshView stopAnimating];
            [weakSelf.tableView reloadData];
        }];
    }];
}

Conclusion

A travers ce tutoriel, nous avons vu comment charger un listing de données depuis une API Web avec un minimum de code en s’appuyant sur différentes librairies du monde Open Source. Ces librairies ont été facilement intégrées au projet à l’aide du Dependency Manager CocoaPods :  AFNetworking, JSONKit, DCKeyValueObjectMapping, SDURLCache, SVPullToRefresh

Les librairies tiers Open Source rendent trivial un développement qui était il y a peu de temps autrement plus complexe à mettre en oeuvre. Elles nous offrent presque gratuitement des fonctionnalités avancées telles que le Pull To Refresh, l’Infinite Scrolling ou bien encore la mise en cache des données avec un minimum d’effort.

Le code source de cet article est bien entendu disponible sur GitHub à l’adresse suivante: https://github.com/xebia-france/UITableViewTutorial

Articles Liés