Depuis la semaine dernière est disponible sur l'AppStore d'Apple une nouvelle application 'powered by OCTO' : Mon Agenda Patrimonial. Application gratuite éditée par la Banque de Gestion Privée IndoSuez (BGPI), elle vous permet de connaître les événements fiscaux et patrimoniaux de l'année, que vous soyez ou non client de la Banque.
Je ne reviendrai pas dans ce billet sur le contenu ou les fonctionnalités de cette application, mais plutôt sur une des ses fonctions annexes : l'intégration avec le calendrier de l'utilisateur. Cette intégration se fait via le framework Event Kit disponible dans le SDK d'Apple.
Si vous voulez savoir comment vous pouvez en quelques lignes de code insérer des évènements dans le calendrier de votre iPhone ou de votre entreprise (même avec Exchange), cet article est fait pour vous.
La première partie de l'article est une introduction à Event Kit avec des exemples de code pour tous les usages simples autour de l'interaction avec un calendrier. Ensuite, dans une deuxième partie, je donne mon retour d'expérience sur le framework avec notamment ses points forts et ses point faibles, selon moi.
EventKit a fait son apparition sur iOS4 et nécessite donc que votre application soit uniquement compatible avec des versions d'iOS > 4 ou que vous gériez dans l'application le cas ou le device est dans une version antérieure de l'OS (on y reviendra plus loin dans l'article).
Le framework EventKit n'est pas ajouté par défaut dans votre projet sous XCode, il faut le faire manuellement. Pour cela, procéder comme habituellement via un click droit sur Frameworks :
Ensuite scrollez pour sélectionner EventKit.framework.
Voilà, il vous reste à inclure le header EventKit.h dans vos classes ou services qui vont accèder au calendrier et vous êtes prêts :
YourClass.m
#import <EventKit/EventKit.h>
...
EventKit est un "petit framework" d'une dizaine de classes avec 3 classes principales : EKEvent, EKEventStore et EKCalendar. En voici une description succinte :
Pour plus de détails sur ces classes, voir la section du site developer.apple.com.
EventKit à un petit frère qui s'appelle EventKitUI et qui vous fournit des vues et controlleurs standards pour pouvoir faire du CRUD d'un événement. On ne parlera pas de ce framework ici car il n'a pas été utilisé sur ce projet mais on reviendra plus loin sur ses manques. Pour plus de détails, voir ici.
Tout se passe via la classe EKEventStore :
EKEventStore *eventStore = [[[EKEventStore alloc] init] autorelease];
NSArray *calendars = [eventStore calendars];
Les propriétés d'intérêt sur EKCalendar sont ensuite son nom (title) et la possibilité d'accès en écriture à ce calendrier (allowsContentModifications).
Si vous souhaitez écrire dans les calendriers de l'utilisateur, il vous faudra donner le choix du calendrier à l'utilisateur. Pour cela un bon défaut est celui que l'utilisateur a choisi comme "calendrier par défaut" sur son iPhone. Vous pouvez le récupérer via la propriété defaultCalendarForNewEvents: :
EKEventStore *eventStore = [[[EKEventStore alloc] init] autorelease];
EKCalendar *defaultCalendar = [eventStore defaultCalendarForNewEvents];
La recherche passe par la classe EKEventStore. La recherche se fait soit via un prédicat sur des propriétés de l'événement soit par une recherche via l'identifiant de l'événement. Un point important est que l'identifiant est unique et que vous pouvez donc l'utiliser quelque soit l'instance de l'EKEventStore.
Voilà comment se fait une recherche via un prédicat dans tous les calendriers :
EKEventStore *eventStore = [[[EKEventStore alloc] init] autorelease];
NSDate startDate = [...];
NSDate endDate = [...];
NSPredicate *predicate = [eventStore predicateForEventsWithStartDate:startDate endDate:endDate calendars:nil]; // nil = all calendars
NSArray *matchingEvents = [eventStore eventsMatchingPredicate: predicate];
Là encore tout passe par la classe EKEventStore :
EKEventStore *eventStore = [[[EKEventStore alloc] init] autorelease];
EKEvent *event = [EKEvent eventWithEventStore:eventStore];
event.title = @"BGPI - Événement de test";
event.startDate = [NSDate date];
event.endDate = [NSDate date];
event.allDay = YES;
event.availability = EKEventAvailabilityFree;
event.notes = @"Événement créé par l'application \'Mon agenda patrimonial\'";
if( ![eventStore saveEvent:event
span:EKSpanThisEvent
error:&error] ) {
NSString *message = [[[NSString alloc] initWithFormat:@"Error adding event %@ in calendar %@: %@", [event title], [defaultCalendar title], [error localizedDescription]] autorelease];
UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:@"Erreur" message:message delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil] autorelease];
[alert show];
}
En plus des champs présentés dans le code ci-dessous, vous pouvez rajouter des participants, des alarmes et une récurrence (se référer aux classes EKParticipant, EKAlarm et EKRecurrenceRule).
La suppression d'un événement se fait via la méthode removeEvent:span:error: de EKEventStore. Il vous faudra par contre avoir une référence de l'événement dans ce store. C'est une subtilité importante, il faut que cet événement ait été récupéré dans le même store, soit par une recherche sur des caractéristiques de l'événement soit par une recherche par son identifiant.
EKEventStore *eventStore = [[[EKEventStore alloc] init] autorelease];
NSError *error;
EKEvent *event = [...];
if( ![eventStore removeEvent:event span:EKSpanThisEvent error:&error]) {
NSLog(@"Error deleting event %@ in calendar: %@",
[event title],
[event.calendar title],
[error localizedDescription]);
}
Le paramètre span est utile dans le cas d'un événement récurrent.
Vous avez une application dont la fonctionnalité principale n'est pas reliée à l'agenda ? Vous pouvez alors choisir de ne pas restreindre vos utilisateurs aux seuls personnes ayant installé iOS4 (même si cela représente déjà plus de 90% du parc).
Voilà comment faire :
if( !NSClassFromString(@"EKEventStore") ){
// do stuff with event kit
}
Notez que vous pouvez également faire ça en testant la version de l'OS via la méthode systemVersion: de la classe UIDevice.
Un bon point de départ sur cette problématique de compatibilité est cet article.
Voilà pour ce qui est des usages simples autour du calendrier. Parlons maintenant un peu des avantages / inconvénients de ce framework et de notre retour d'expérience sur ce dernier.
Clairement, un des gros points fort est que l'API est très simple.
Le développeur accède de plus à tous les calendriers indépendamment du provider du calendrier (Google Calendar, Exchange, Lotus, le calendrier de l'iPhone en local etc...) et du protocole d'accès (CalDAV, propriétaire etc...).
Un autre point fort du SDK est que les calendriers distants sont "cachés" localement et que donc l'accès à un calendrier distant offline est transparent pour le développeur, il n'a pas besoin de se soucier de la connectivité.
Dis comme ça ces points peuvent paraître anodins mais ce sont des avantages extrêmement forts pour le développeur. Comprenez par là que même si je m'étends moins longuement sur cette section que sur la suivante, je suis pour autant convaincu de la valeur d'EventKit pour le développeur.
Une des forces de l'API est sa simplicité. Simple ne veut pas dire limité mais dans le cas d'EventKit, c'est malheureusement un peu le cas.
Cela s'est concrétisé dans mon expérience sur deux volets :
Vous vous retrouvez donc à faire des choses du type :
[...]
// First filter on date if possible
NSPredicate *limitedPredicate = [eventStore predicateForEventsWithStartDate:startDate endDate:endDate calendars:nil];
NSArray *partiallyMatchingEvents = [eventStore eventsMatchingPredicate: limitedPredicate];
// Then filter on properties
NSPredicate *fullPredicate = [NSPredicate predicateWithFormat:@"title == %@", @"Titre à rechercher"];
NSArray *matchingEvents = [partiallyMatchingEvents filterUsingPredicate:fullPredicate];
Ce qui est tout de même malheureux et surtout handicapant d'un point de vue performances. Dans notre cas, on ne pouvait pas se permettre par exemple à chaque mise à jour de rechercher sur toute l'année en cours dans le calendrier et il a donc été décidé de persister les identifiants des événements afin de pouvoir y accèder rapidement (via la méthode eventWithIdentifier:).
J'en parlais au début, le framework EventKitUI est pour moi un peu léger et c'est tout de même malheureux de devoir refaire l'écran de choix du calendrier pourtant déjà disponible dans l'application Apple Calendrier ! A quand son inclusion dans EventKitUI ?
Dernier point et non des moindres, on a rencontré des soucis de fiabilité, certainement dûs à un framework un peu jeune ? Dans notre cas d'usage, c'est-à-dire l'insertion en batch d'événements dans un calendrier, il se produisait dans une fraction des cas des erreurs silencieuses. Concrètement, en insérant de manière séquentielle une cinquantaine d'événements dans le calendrier, il manquait dans la plupart des cas, et sans erreur retournée par l'API (saveEvent:span:error:), un événement sur les cinquante. Un bug a été ouvert chez Apple à ce sujet (Bug ID# 9405210).
Ce problème a été contourné en vérifiant après insertion que l'événement était effectivement dans le calendrier via la méthode refresh: mais cet appel a un coût en termes de performances.
En parlant de performance, il se trouve que le framework est un peu lent. Quelques temps très inexacts, expérimentés sur un iPhone 3G en iOS 4.2.1 :
Sans vérification | Avec vérification | |
---|---|---|
Temps d'insertion de 50 événements | 20 s | 23 s |
Là encore mon analyse est que l'API a été trop simplifiée. Par exemple il manque clairement une méthode d'insertion en batch (un saveEvents: !), notre cas d'usage étant très certainement pénalisé par des locks pour accèder au calendrier, autant de fois qu'il y a d'événements à insérer.
Pensez donc, selon votre cas d'usage, à déléguer l'insertion des événements à un autre thread que la thread principale afin de ne pas bloquer l'interface (par exemple via la méthode detachNewThreadSelector: de la classe NSThread).
J'espère que cet article vous aura donné un bon aperçu du framework EventKit et quelques retours concrets sur le framework. Les cas d'usages sont nombreux, que ce soit par exemple pour des applications administratives (rappel d'échéances, rappel d'un rendez-vous avec votre banquier...), ou des applications de productivité personnelle.
Et vous, quel est votre REX sur ce framework ?