Comment synchroniser Core Data avec Azure

Synchroniser Core Data avec Azure est-ce possible ? En théorie, oui.
Mais à quel prix ? Quelles sont les étapes à réaliser ?
Réponses, explorations et essais techniques.
Tout développer soi-même. C’est par là qu’il faut passer si l’on veut utiliser un autre cloud provider que iCloud pour synchroniser une application utilisant Core Data. La synchronisation est en effet à réaliser soi-même via des webservices.
Le fil directeur de cette série d’articles est la création d’une application iOS d’annuaire partagé. La synchronisation de l’annuaire se fera entre les devices et une base de données stockées dans Azure.
Créer une application iOS avec Core Data
Core Data est extrêmement simple à utiliser. Sa grande force : très peu d’étapes à mettre en place.
Explorations et explications.
Core Data est la technologie de référence pour la persistance des données des applications iOS. Présente depuis la version 3.0 du SDK d’iPhone. Celle-ci a su évoluer pour devenir de plus en plus facile à utiliser.
Ce premier article va permettre d’expliquer les différentes étapes de la création d’un projet iOS avec Core Data et de soulever quelques points d’attention.
On va parcourir ces différentes étapes en réalisant la première version de l’application annuaire. Cette version va permettre de gérer des contacts et de les stocker en local sur le téléphone.
1ère étape : la prise en main de Core Data
Qu’est-ce que Core Data ?
Core Data est un framework qui facilite la persistance des données. Il est inclus dans le SDK d’iOS. C’est une base de données locale munie d’une API orientée objet. Il permet de stocker les données d’une application pour un usage offline ou pour stocker des données temporaires. Il se base sur un modèle de données, ce dernier représente les objets que l’on veut stocker. Il permet de créer la définition de ces objets ainsi que les relations qui existent entre eux. Grâce à Xcode, ce modèle s’édite de manière graphique. Les classes qui permettent de manipuler ces objets depuis le code peuvent être générées automatiquement. Core Data gère les instances de ces objets au runtime.
La fonctionnalité principale de Core Data est donc la persistance des données en permettant l’abstraction de la base de données. Il gère pour nous l’administration de cette base.
Core Data vient également avec d’autres fonctionnalités très pratiques :
- Annuler et rétablir des modifications : Core Data garde une trace des différents changements. Il peut facilement annuler les derniers changements effectués ou les réappliquer.
- Exécuter des modifications par lots : afin d’optimiser les traitements de grosse quantité de données, Core Data réalise des suppressions ou des modifications par lots.
- Exécuter des tâches en background pour ne pas bloquer l’UI : afin d’exécuter des traitements longs comme récupérer des données depuis un web service et passer du Json.
- Version et migration : Core Data vient avec des mécanismes qui simplifient le versioning du modèle de données. Il facilite également la création des migrations nécessaires.
- Synchronisation avec la vue : Core Data fournit une source de données pour les TableView et CollectionView avec l’utilisation du NSFetchResultController.
Installer Core Data
La prise en main de Core Data est d’une simplicité déconcertante. Il suffit de choisir un template d’application iOS et de sélectionner l’option Use Core Data pour avoir la stack Core Data déjà initialisée.
XCode rajoute automatiquement plusieurs éléments dans le projet iOS pour la gestion de Core Data :
- Un fichier annuaire.xcdatamodeld qui va permettre de construire le modèle de données,
- Deux méthodes dans l’app delegate pour manipuler Core Data.
La première méthode est l’initialisation de la stack Core Data. Depuis iOS 10 et l’apparition du NSPersistentContainer c’est vraiment l’affaire de deux lignes de code. Il suffit d’instancier un NSPersistentContainer avec le nom de notre fichier xcdatamodeld puis d’appeler la méthode loadPersistentStores de ce container. Le NSPersistentContainer s’occupe par défaut d’initialiser la base de données SQL Lite locale avec comme nom celui de notre modèle. Tout est bien sûr paramétrable. On peut changer le nom de la base de données ainsi que son emplacement. On peut également choisir un autre type de stockage comme du XML, du Binaire ou même du in-memory.
La deuxième méthode saveContext permet de sauvegarder les modifications apportées au viewContext du NSPersistentContainer.
L’arrivée de NSPersistentContainer a vraiment simplifié l’usage de Core Data. Il est cependant intéressant de comprendre ce qu’il se passe dessous. Pour en savoir plus, je vous conseille de lire l’article d’Open classroom sur la persistance des données dans votre application iOS.
Ecrire l’entity Contact
Le but de l’application d’annuaire partagé va être d’ajouter, de modifier et de supprimer des contacts ainsi que de synchroniser cette liste de contact entre devices via un stockage dans le cloud Azure.
Les objets manipulés par l’application sont donc des contacts. L’objet contact a un nom, un prénom, un numéro de téléphone et une adresse email.
Les objets dans Core Data sont nommés Entities, ils correspondent à la définition de l’objet comme si on définissait une classe. Ils sont d’ailleurs représentés ainsi dans le code.
Les propriétés de ces objets sont des attributs et ont chacun un type.
Pour ajouter l’entity Contact au modèle, on ouvre le fichier Annuaire.xcdatamodeld et on clique sur Add Entity.
On ajoute ensuite les différents attributs nécessaires en spécifiant leur type. On obtient alors le modèle suivant :
Un attribut « id » est ajouté afin d’identifier de manière unique les objets. La gestion de cet id soulève de nombreuses questions par rapport à la synchronisation. On détaillera ces problématiques dans une prochaine partie avec les réponses à y apporter.
La partie Relationships permet de créer les relations entre les différents objets. Les Fetched Properties sont des propriétés qui seront calculées grâce à un predicat.
Pour garder l’application le plus simple possible, le seul objet manipulé est l’objet Contact et il ne possède aucune relation.
L’étape suivante est de générer la classe associée à cet objet pour pouvoir la manipuler depuis le code. Les objets récupérés sont du type NSManagedObject. Plusieurs options existent. Ces options peuvent être trouvées dans les utilitaires de droite après avoir sélectionné l’entité Contact. C’est au niveau du réglage de Codegen (pour Code Generation ou génération automatique de code) que le choix du type de génération se fait.
3 valeurs sont possibles :
- Manual/None
- Class Definition
- Category/Extension
Depuis XCode 8, la valeur Codegen par défaut est à Class Definition. Auparavant, elle était à Manual/None.
L’option Manual/None ne crée pas de code exposant l’entity Contact sous forme de NSManagedObject. Il faut créer les classes manuellement pour pouvoir manipuler les objets.
L’option Class Definition crée la définition de la classe Contact qui va hériter de NSManagedObject. Il suffit ensuite de manipuler l’objet Core Data.
L’option Category/Extension est à mi-chemin entre le Manual/None et le Class Definition. Il faut créer une classe Annuaire, en parallèle XCode va générer une extension de cette classe qui assure la conformité avec NSManagedObject. Il sera possible avec cette option d’ajouter de nouvelles fonctions dans la classe Contact pour enrichir l’objet.
Dans le cas de l’application annuaire que l’on crée, l’option Class Definition est suffisante.
Voici le détail des réglages de l’entity Contact.
Il est important de préciser qu’afin d’éviter des problèmes de récupération d’objet il est préférable de paramétrer la propriété Module à Current Product Module plutôt que de laisser la valeur par défaut qui est Global namespace.
Pour obtenir plus d’informations sur les autres propriétés voici le lien vers la documentation Apple : https://developer.apple.com/documentation/coredata/modeling_data/configuring_entities
Maintenant que Core Data est paramétré et l’entity Contact configurée, il est temps de créer et d’afficher nos données.
Il est temps maintenant de créer et de gérer les contacts depuis l’application.
Il faut pour cela plusieurs écrans : un écran avec la liste des données, un écran avec un formulaire pour créer un nouveau contact et un écran pour modifier un contact.
2ème étape : Créer les écrans pour gérer les Contacts
Les écrans sont les plus simples possible pour répondre au besoin.
Le premier écran affiche la liste des contacts sous forme d’une TableView. Le bouton + navigue vers l’écran d’ajout d’un contact qui contient une stackView de TextField. Le bouton enregistrer sauvegarde le Contact et revient vers l’écran de liste qui rafraichit sa liste.
Un troisième écran permet de modifier un contact déjà existant. Il ressemble à l’écran de création d’un Contact. Un seul écran ainsi qu’un seul ViewController pourrait être utilisé pour réaliser la création et la modification. Un segue « editContact » permet de naviguer depuis la liste de contact vers l’écran de modification. Un NavigationController sert de point d’entrée à notre application et permet la navigation entre les différents écrans.
On réalise l’architecture suivante pour le projet iOS.
On retrouve les trois ViewControllers qui correspondent aux trois écrans. Le main.storyboard contient les différents écrans. Le dossier Model contient notre modèle de données ainsi qu’une classe de service ContactService qui va contenir les fonctions de récupération, d’update et suppression d’un contact.
C’est dans cette classe que toute la magie liée à CoreData opère.
Détails du ContactService
Le ContactService est l’endroit où Core Data est réellement manipulé.
Quand on parle de Core Data et de manipulation d’objet tout se passe dans un context. On a aperçu cette notion dans les méthodes ajoutées par XCode dans l’AppDelegate.
Un contexte dans CoreData permet de sauvegarder en mémoire toutes les actions qui ont été faite dans ce contexte. Mais tant qu’il n’est pas sauvegardé rien ne sera réellement stocké dans la base de données locale. C’est-à-dire que si on ferme l’application sans avoir sauvegardé le contexte toutes les modifications seront perdues. C’est une sorte de tampon avant la base de données.
Plusieurs context et type de context peuvent être créer dans une application. Mais le passage des objets d’un context à un autre repose sur une mécanique de merge. Ces points seront abordés au moment de la synchronisation avec les webservices. L’utilisation du viewContext du PersistantContainer créée dans l’AppDelegate est parfaitement adapté pour la première version de l’application.
Pour sauvegarder un context il suffit d’appeler la méthode save sur ce contexte. Une méthode saveContext existe déjà dans l’AppDelegate pour sauvegarder le viewContext du persistentContainer.
La méthode add permet de créer un nouvel objet Contact. Elle crée une instance de la classe Contact. L’initialiseur cette classe requiert un context en paramètre. On récupère le viewContext de l’appDelegate et on écrit cette ligne de code :
Et voilà, on vient de créer un objet Core Data. Il est maintenant possible de le manipuler et d’attribuer des valeurs à ses attributs.
La méthode remove supprime un objet Contact. Pour supprimer un objet de CoreData il suffit d’appeler la méthode delete du viewContext avec l’instance de l’objet à supprimer. L’instance de cet objet est alors supprimée du context.
La méthode update est appelée lors de la mise à jour d’un contact. Elle ne fait rien à part sauvegarder le context. En effet, on manipule directement les objets du context. Ces objets sont stockés en mémoire. Toute modification d’une des propriétés de l’objet modifie automatiquement l’objet.
Pour le moment le contexte est sauvegardé à chaque modification apportée. Pour manipuler des objets d’un contexte il est nécessaire de rester dans le même thread c’est-à-dire dans notre cas dans le threadUI. A terme des problèmes de performance et de blocage de l’UI pourraient survenir si la base de données venait à beaucoup grossir. Des stratégies pour réduire le nombre de sauvegarde du contexte pourraient être appliquées (à la fermeture de l’application, tous les X temps).
La méthode getAll retourne tous les objets Contacts. Pour requêter les objets au sein de Core Data il faut utiliser des NSFetchRequest sur un type d’entité. La requête let request: NSFetchRequest<Contact> = Contact.fetchRequest() permet de retourner tous les objets de type Contact. Les NSFetchRequest peuvent être paramétrées avec des NSPredicate afin de filtrer les objets retournés ou de les ordonner. On se retrouve vraiment avec les fonctionnalités de Linq dans le monde C#. Des requêtes plus pointues seront abordées lors de la synchronisation des données. Cette requête s’effectue toujours par rapport à un contexte. Les objets présents dans un contexte peuvent être différents de ceux actuellement stockés en base de données si le contexte n’a pas été sauvegardé.
Détails des ViewControllers
Les ViewControllers n’ont pas de spécificité particulière. Ils utilisent le service ContactService pour récupérer et manipuler les objets contacts.
Le ContactListViewController est un UITableViewController. Il récupère la liste de Contact à afficher via l’appel à la méthode getAll du ContactService.
Un swipe to delete est paramétré grâce au style éditable delete. Il appelle la méthode remove du ContactService.
Un swipe to edit est paramétré en utilisant la méthode “leadindSwipeActionsConfigurationForRowAt”. Le swipe déclenche le segue editContact et passe l’objet Contact à modifier au EditViewController.
Le AddContactViewController permet de récupérer les champs renseignés par l’utilisateur. Le bouton Enregistrer de l’écran déclenche l’appel de la méthode save qui passent les valeurs des TextView à la méthode add du ContactService. L’écran est ensuite fermé.
Le EditViewController reçoit un objet Contact depuis le ContactListViewController. Les propriétés de cet objet permettent de préremplir les champs à modifier. Le bouton Modifier de l’écran déclenche l’appel de la méthode save. Cette méthode modifie directement les attributs de l’objet contact du ViewController. Cet objet vient de la liste des Contacts récupérés depuis le ContactListViewController. C’est donc un objet Core Data. La modification de ses propriétés est donc automatiquement enregistrée dans le viewContext. L’appel à la méthode update du ContactService permet de persister les modifications du context. L’écran est ensuite fermé.
Déporter la logique de manipulation de la donnée depuis CoreData
A la fin de cette première partie l’application d’annuaire développée permet de créer, modifier et supprimer des contacts. Ces contacts sont stockés en local sur notre device iOS dans une base de données SQLite grâce à Core Data.
Core Data apparait d’une simplicité déconcertante à utiliser. Il suffit vraiment de très peu de ligne de code pour avoir un stockage de données local robuste et facilement manipulable.
Core Data est l’équivalent d’Entity Framework mais inclus directement dans le framework d’iOS. Il est donc accessible sans l’ajout d’une librairie supplémentaire. Il n’est donc pas nécessaire de choisir entre simplicité et poids de l’application comme sur Xamarin.
L’architecture que propose iOS avec Core Data notamment avec l’usage du NSFetchController est source de discussion. Le NSFetchController permet de créer une source de données pour une TableView ou une CollectionView directement depuis une requête CoreData. C’est donc le Controller qui manipule la donnée à afficher. Ce couplage va à l’encontre du pattern de développement MVVM. La classe de service a été créée pour déporter la logique de manipulation de la donnée depuis CoreData ailleurs que dans les Controllers mais on pourrait aller beaucoup plus loin.
La prochaine partie portera sur la synchronisation de l’application avec un webservice afin de stocker les objets à distance.