Sur la route d'Oxiane digressions diverses

LeBlog OXiane

8 fév
2012

iOS & Core Data

Nous allons encore poursuivre avec notre application développée lors de mes 2 derniers billets ici et afin d’ajouter un bouton « clickCoreData » qui fera un segue vers un UITableViewController peuplé à partir de Core Data.

Qu’est ce que Core Data ?

« C’est une bibliothèque permettant de gérer le cycle de vie des objets et les graphes d’objets de manière automatique et générale incluant également la persistance » – traduction partielle de la page du site d’Apple.

En gros, pour moi c’est un peu comme une base de données orientée objet, on pourrait faire la parallèle avec Hibernate côté Java sauf que ce n’est pas un framework de persistance d’objet même s’il peut persister les graphes d’objets (relations et états).

Ici, nous allons créer le mapping entre la base de donnée et les objets puis les requêtes permettant de récupérer/modifier/… nos objets et enfin accéder aux « colonnes de notre table » grâce aux propriétés de ces objets.

Retour sur le delegate

Mais avant tout ça, il faut quand même que nous revenions sur la délégation.

En effet, nous avons utilisé 2 fois ce pattern afin de permettre la transmission d’une donnée au contrôleur parent et la disparition de la vue courante faîte par le contrôleur parent.

Seulement dans notre PageTrickViewController, nous avons deux propriétés correspondant aux deux types de contrôleurs affichables et si nous voulons ajouter 3, 4 ou 15 autres contrôleurs nous allons devoir ajouter 3,4 ou 15 autres propriétés.

Il faut donc appliquer le pattern jusqu’au bout pour résoudre cela et ne plus avoir qu’ 1 propriété.
Pour y arriver, il suffit que les délégants soit conforme au même protocole que PageTrickViewController !

@interface PageCurlController : UIViewController<PageCurlDelegate>

@interface TablePageCurlController : UITableViewController<PageCurlDelegate>

Ainsi, on peut remplacer les propriétés par une seule dans le PageTrickViewController:

@property(nonatomic, strong) id<PageCurlDelegate> universalPage;

On n’oublie pas de synthétiser :

@synthesize universalPage = _universalPage;

Bien sûr, il va falloir modifier prepareForSegue et dismissWithData:

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([segue.identifier isEqualToString:@"tableModalCurl"]){
        _universalPage = segue.destinationViewController;
        if([_universalPage isKindOfClass:[TablePageCurlController class]]){
            [_universalPage performSelector:@selector(setDelegate:) withObject:self];
            Sqlite *sqlite = [[Sqlite alloc] init];

            NSString *dbPath = [[NSBundle mainBundle] pathForResource:@"obj" ofType:@"db"];

            if(![sqlite open:dbPath])
                return;
            NSArray *objs = [sqlite executeQuery:@"SELECT * FROM Objet"];
            [_universalPage performSelector:@selector(setRows:) withObject:[NSNumber numberWithInt:objs.count]];
            [_universalPage performSelector:@selector(setObjects:) withObject:objs];
        }
    }else if([segue.identifier isEqualToString:@"modalCurl"]){
        _universalPage = segue.destinationViewController;
        if([_universalPage isKindOfClass:[PageCurlController class]]){
            [_universalPage performSelector:@selector(setDelegate:) withObject:self];
        }
    }
}

-(void)dismissWithData:(NSString *)data{
    NSLog(@"%@", data);
    //éh oui, on est obligé de passer un NSNumber avec ARC
    [_universalPage performSelector:@selector(dismissModalViewControllerAnimated:) withObject:[NSNumber numberWithBool:YES]];
    _universalPage = nil;

}

Passons à Core Data !

Comment l’utilise t-on ?

Eh bien c’est assez simple, nous allons créer nos entités grâce à Xcode puis y accéder avec un NSManagedObjectContext, l’objet central autour duquel tourne toute l’activité de Core Data.

1. Importation du framework :

Comme expliquer dans mon précédent billet, faîtes les étapes permettant d’ajouter
un framework à votre projet.
Ici nous devons importer : CoreData.framework

2. Création visuelle de la base de données :

Tout en vidéo :

Et oui alors, c’est quoi @dynamic ?
Pour tout vous dire c’est un peu comme @synthesize sauf que l’on dit au compilateur : « ne t’inquiète pas, je te filerai les implémentations des accesseurs à l’exécution ou directement ».

Mais comme tout est bien fait dans iOS (enfin presque), Core Data génère lui-même les accesseurs pour nous à l’exécution… si c’est pas sympa tout ça !

On peut donc employer la notation pointée sur nos entités les yeux fermés mais avant cela précisons un peu plus comment fonctionne Core Data.

En fait, il y a 2 façons d’utiliser ce framework :
– soit on crée un projet en cochant la case « use Core Data » (ce qu’on n’a pas fait au début… flûte !)
– soit on utilise UIManageDocument et on récupère son NSManagedObjectContext

On opte donc pour la 2ème solution, d’ailleurs elle est très pratique car c’est une porte ouverte sur iCloud mais je n’en dis pas plus pour l’instant à ce sujet. (suspens !)

Il faut voir UIManageDocument comme un conteneur pour notre base de données.

Afin de faciliter le codage, j’ai utilisé une classe mise à disposition ici par Paul Hegarty de l’Université de Stanford.
Rassurez-vous le zip de mon projet est disponible en bas de page.

Cette classe facilite la gestion d’un UITableViewController dont les cellules sont peuplées en fonction de nos entités car pour les remplir nous avons besoin d’un NSFetchedResultController capable de répondre au protocole UITableViewDataSource.

Par exemple, une des méthodes importante de NSFetchedResultController est :

(NSManagedObject *)objectAtIndexPath:(NSIndexPath *)indexPath

qui irait très bien dans :

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

Une fois cette classe importer dans notre projet, il ne nous reste plus qu’à la sous-classer en créeant notre classe CoreDataTablePageCurlController :

Interface :

#import "CoreDataTableViewController.h"
#import "PageCurlDelegate.h"

@interface CoreDataTablePageCurlController : CoreDataTableViewController<PageCurlDelegate>

@property(nonatomic, weak) id<PageCurlDelegate> delegate;
@property (nonatomic, weak) UIManagedDocument *database;

@end

Implémentation :

#import "CoreDataTablePageCurlController.h"
#import "Objet.h"

@implementation CoreDataTablePageCurlController

@synthesize delegate = _delegate, database = _database;

#pragma mark - Utility methods

- (void)setupFetchedResultsController // attaches an NSFetchRequest to this UITableViewController
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Objet"];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"num"ascending:YES]];
    // no predicate because we want ALL the Objects

    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                        managedObjectContext:self.database.managedObjectContext
                                                                          sectionNameKeyPath:nil
                                                                                   cacheName:nil];
}

// Open or create the document here and call setupFetchedResultsController

- (void)useDocument
{
    if (![[NSFileManager defaultManager] fileExistsAtPath:[self.database.fileURL path]]) {

        // c'est ici que commence la création d'objet
        NSEntityDescription *objetDesc = [NSEntityDescription entityForName:@"Objet" inManagedObjectContext:self.database.managedObjectContext];

        Objet *cercle = [[Objet alloc] initWithEntity:objetDesc insertIntoManagedObjectContext:self.database.managedObjectContext];
        cercle.num = [NSNumber numberWithInt:1];
        cercle.forme = @"cercle";

        Objet *carre = [[Objet alloc] initWithEntity:objetDesc insertIntoManagedObjectContext:self.database.managedObjectContext];
        carre.num = [NSNumber numberWithInt:2];
        carre.forme = @"carre";

        Objet *triangle = [[Objet alloc] initWithEntity:objetDesc insertIntoManagedObjectContext:self.database.managedObjectContext];
        triangle.num = [NSNumber numberWithInt:3];
        triangle.forme = @"triangle";

        NSError *error;

        // on sauvegarde le contexte
        if (![self.database.managedObjectContext save:&error]) {
            NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
        }
        // on crée la abase de donnée grâce à ce message
        [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {

            [self setupFetchedResultsController];
        }];
    } else if (self.database.documentState == UIDocumentStateClosed) {
        // exists on disk, but we need to open it
        [self.database openWithCompletionHandler:^(BOOL success) {
            [self setupFetchedResultsController];
        }];
    } else if (self.database.documentState == UIDocumentStateNormal) {
        // already open and ready to use
        [self setupFetchedResultsController];
    }
}

#pragma mark - Accessor method

// 2. Make the database's setter start using it

- (void)setDatabase:(UIManagedDocument *)database
{
    if (_database != database) {
        _database = database;
        [self useDocument];
    }
}

#pragma mark - TableView dataSource & delegate methods

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"myCell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

    Objet *object = [self.fetchedResultsController objectAtIndexPath:indexPath];

    cell.textLabel.text = object.forme;

    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self dismissWithData:[tableView cellForRowAtIndexPath:indexPath].textLabel.text];
}

#pragma mark - View lifecycle

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    if (!self.database) {  // for demo purposes, we'll create a default database if none is set
        NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
        url = [url URLByAppendingPathComponent:@"Default_Database"];
        // url is now "<Documents Directory>/Default_Database"
        self.database = [[UIManagedDocument alloc] initWithFileURL:url]; // setter will create this for us on disk
    }
}

-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self.tableView setContentOffset:CGPointMake(0, -self.tableView.frame.size.height/4) animated:YES];
    //[self.tableView reloadData];
    //[self.view setNeedsDisplay];
}

#pragma mark - PageCurlDelegate protocol method

- (void)dismissWithData:(NSString *)data
{
    [self.delegate dismissWithData:data];
}

@end

J’ai fait quelques brefs commentaires dans le code afin de montrer les différentes étapes mais détaillons un peu…
Comme notre base de données est vide au début, on récupère une description de l’entité qui nous intéresse (ici « Objet ») :

NSEntityDescription *cercleDesc = [NSEntityDescription entityForName:@"Objet" inManagedObjectContext:self.database.managedObjectContext];

puis on instancie l’objet tout en l’ajoutant au contexte :

Objet *cercle = [[Objet alloc] initWithEntity:cercleDesc insertIntoManagedObjectContext:self.database.managedObjectContext];

ensuite on définit c’est champs grâce à la notation pointée, en effet rappelez-vous ce que j’ai dit tout à l’heure : @dynamic indique au compilateur que l’implémentation des accesseurs lui sera fournit plus tard à l’exécution et justement Core Data l’a fait pour nous !

cercle.num = [NSNumber numberWithInt:1];
cercle.forme = @"cercle";

enfin, il ne reste plus qu’à sauver le contexte et créer la base de données :

if (![self.database.managedObjectContext save:&error]) {
            NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
        }
        // does not exist on disk, so create it
        [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {

            [self setupFetchedResultsController];
        }];

Ah mais on oublierait pas de parler de requête par hasard ? Les objets ne vont pas arriver tous seuls comme par magie !
Bah non, c’est sûr.
Si on regarde dans le block passé en argument de ce message, on tombe sur l’envoi du message setupFetchedResultsController.
Dans la méthode setupFetchedResultsController, on fait toutce qu’il faut :

création de la requête pour l’entité Objet
[/objc]
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Objet"];
[/objc]

on trie les objets selon leur attribut num

request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"num"ascending:YES]];

on ne définit pas de prédicat car on désire récupérer tous les objets, en revanche on n’oublie pas d’instancier notre NSFetchedResultsController

self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                        managedObjectContext:self.database.managedObjectContext
                                                                          sectionNameKeyPath:nil
                                                                                   cacheName:nil];

pour plus de détails sur les requêtes voir la doc.

Une fois terminé, ajoutez dans votre storyboard :
– un bouton « clickCoreData »
– un UITableViewController, dont le type sera évidemment CoreDataTablePageCurlController
– un segue de ce bouton vers le UITableViewController avec comme transition « Cover vertical » (pour changer, en plus c’est joli ça fait un effet rebond avec le offset)

Oups, nous allions oublier de mettre à jour notre méthode prepareForSegue dans PageTrickViewController :

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{

    if([segue.identifier isEqualToString:@"coreDataModalCurl"]){
        _universalPage = segue.destinationViewController;
        if([_universalPage isKindOfClass:[CoreDataTableViewController class]]){
            NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
            url = [url URLByAppendingPathComponent:@"Default_Database"];
            // création du document
            UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:url];
            // transmission du document au CoreDataTableViewController
            // la base de données est remplie dans celui-ci et le document sauvegardé aussi
            [_universalPage performSelector:@selector(setDatabase:) withObject:document];
            [_universalPage performSelector:@selector(setDelegate:) withObject:self];
        }

    }
    else if([segue.identifier isEqualToString:@"tableModalCurl"]){
        _universalPage = segue.destinationViewController;
        if([_universalPage isKindOfClass:[TablePageCurlController class]]){
            [_universalPage performSelector:@selector(setDelegate:) withObject:self];
            Sqlite *sqlite = [[Sqlite alloc] init];

            NSString *dbPath = [[NSBundle mainBundle] pathForResource:@"obj" ofType:@"db"];

            if(![sqlite open:dbPath])
                return;
            NSArray *objs = [sqlite executeQuery:@"SELECT * FROM Objet"];
            [_universalPage performSelector:@selector(setRows:) withObject:[NSNumber numberWithInt:objs.count]];
            [_universalPage performSelector:@selector(setObjects:) withObject:objs];
        }
    }else if([segue.identifier isEqualToString:@"modalCurl"]){
        _universalPage = segue.destinationViewController;
        if([_universalPage isKindOfClass:[PageCurlController class]]){
            [_universalPage performSelector:@selector(setDelegate:) withObject:self];
        }
    }

}

Le mal est réparé !

Et voici la petite vidéo et le projet zippé en bonus :

Manuel François

tHeFeaTuReDMaN