Comment synchroniser de manière asynchrone CoreData et un service Web REST et propager correctement toutes les erreurs REST dans l’interface utilisateur

Hé, je travaille sur la couche modèle pour notre application ici.

Certaines des exigences sont comme ceci:

  1. Il devrait fonctionner sur iPhone OS 3.0+.
  2. La source de nos données est une application RESTful Rails.
  3. Nous devrions mettre en cache les données localement en utilisant les données de base.
  4. Le code client (nos contrôleurs d’interface utilisateur) devrait avoir le moins de connaissances possible sur les éléments du réseau et devrait interroger / mettre à jour le modèle avec l’API Core Data.

J’ai vérifié la session WWDC10 117 sur la création d’une expérience utilisateur pilotée par le serveur, et j’ai passé du temps à vérifier les frameworks Objective Resource , Core Resource et RestfulCoreData .

Le framework Objective Resource ne communique pas seul avec les données de base et est simplement une implémentation de client REST. Les ressources de base et RestfulCoreData supposent toutes que vous communiquez avec les données de base de votre code et qu’elles résolvent tous les problèmes en arrière-plan de la couche modèle.

Tout semble correct jusqu’à maintenant et au départ, je pensais que Core Resource ou RestfulCoreData couvriraient toutes les exigences ci-dessus, mais …

  1. Le thread principal ne doit pas être bloqué lors de l’enregistrement des mises à jour locales sur le serveur.
  2. Si l’opération de sauvegarde échoue, l’erreur doit être propagée à l’interface utilisateur et aucune modification ne doit être enregistrée dans le stockage Core Data local.

Lorsque vous appelez - (BOOL)save:(NSError **)error dans votre contexte d’object géré, Core Resource émet toutes ses requêtes sur le serveur et peut donc fournir une instance NSError correcte des requêtes sous-jacentes au serveur. en quelque sorte. Mais il bloque le thread d’appel jusqu’à ce que l’opération de sauvegarde se termine. ÉCHOUER.

RestfulCoreData conserve votre -save: appels intacts et n’introduit aucun temps d’attente supplémentaire pour le thread client. Il surveille simplement le NSManagedObjectContextDidSaveNotification , puis NSManagedObjectContextDidSaveNotification les requêtes correspondantes au serveur dans le gestionnaire de notifications. Mais de cette manière, le -save: call se termine toujours avec succès (eh bien, étant donné que les données de base sont correctes avec les modifications enregistrées) et que le code client réellement appelé n’a aucun moyen de savoir si la sauvegarde a échoué 404 ou 421 ou toute erreur côté serveur s’est produite. Et plus encore, le stockage local devient la mise à jour des données, mais le serveur ne connaît jamais les modifications. ÉCHOUER.

Donc, je cherche une solution possible / des pratiques communes pour faire face à tous ces problèmes:

  1. Je ne veux pas que le thread appelant bloque sur chaque -save: appel pendant que les requêtes réseau se produisent.
  2. Je veux en quelque sorte recevoir des notifications dans l’interface utilisateur indiquant que certaines opérations de synchronisation ont échoué.
  3. Je souhaite que la sauvegarde des données de base réelle échoue également si les requêtes du serveur échouent.

Des idées?

Vous devriez vraiment regarder RestKit ( http://restkit.org ) pour ce cas d’utilisation. Il est conçu pour résoudre les problèmes de modélisation et de synchronisation des ressources JSON distantes sur un cache local Core Data sauvegardé. Il prend en charge un mode hors connexion pour travailler entièrement à partir du cache lorsqu’il n’y a pas de réseau disponible. Toute la synchronisation se produit sur un thread d’arrière-plan (access au réseau, parsing de charge utile et fusion de contexte d’object géré) et il existe un vaste ensemble de méthodes de délégation pour que vous puissiez savoir ce qui se passe.

Il y a trois composants de base:

  1. L’action de l’interface utilisateur et le changement persistant de CoreData
  2. Persistance de ce changement au serveur
  3. Actualiser l’interface utilisateur avec la réponse du serveur

Un NSOperation + NSOperationQueue aidera à garder les requêtes réseau ordonnées. Un protocole délégué aidera vos classes d’interface utilisateur à comprendre l’état des requêtes réseau, par exemple:

 @protocol NetworkOperationDelegate - (void)operation:(NSOperation *)op willSendRequest:(NSURLRequest *)request forChangedEntityWithId:(NSManagedObjectID *)entity; - (void)operation:(NSOperation *)op didSuccessfullySendRequest:(NSURLRequest *)request forChangedEntityWithId:(NSManagedObjectID *)entity; - (void)operation:(NSOperation *)op encounteredAnError:(NSError *)error afterSendingRequest:(NSURLRequest *)request forChangedEntityWithId:(NSManagedObjectID *)entity; @end 

Le format du protocole dépendra bien sûr de votre cas d’utilisation spécifique, mais essentiellement, ce que vous créez est un mécanisme par lequel les modifications peuvent être “poussées” sur votre serveur.

Ensuite, la boucle d’interface utilisateur à prendre en compte, pour garder votre code propre, il serait bien d’appeler save: et de faire en sorte que les modifications soient automatiquement transmises au serveur. Vous pouvez utiliser les notifications NSManagedObjectContextDidSave pour cela.

 - (void)managedObjectContextDidSave:(NSNotification *)saveNotification { NSArray *inserted = [[saveNotification userInfo] valueForKey:NSInsertedObjects]; for (NSManagedObject *obj in inserted) { //create a new NSOperation for this entity which will invoke the appropraite rest api //add to operation queue } //do the same thing for deleted and updated objects } 

La surcharge de calcul pour l’insertion des opérations réseau devrait être plutôt faible, cependant, si cela crée un décalage notable sur l’interface utilisateur, vous pouvez simplement extraire les identifiants d’entité de la notification de sauvegarde et créer les opérations sur un thread d’arrière-plan.

Si votre API REST prend en charge le traitement par lots, vous pouvez même envoyer l’intégralité du tableau en même temps, puis vous informer de la synchronisation de plusieurs entités.

Le seul problème que je prévois, et pour lequel il n’existe pas de «vraie» solution, est que l’utilisateur ne souhaite pas attendre que ses modifications soient envoyées au serveur pour pouvoir apporter d’autres modifications. Le seul bon paradigme que j’ai rencontré est que vous permettez à l’utilisateur de continuer à éditer les objects et à regrouper leurs modifications, le cas échéant, c.-à-d.

Cela devient un problème de synchronisation et non un problème facile à résoudre. Voici ce que je ferais: Dans votre interface utilisateur iPhone, utilisez un contexte, puis utilisez un autre contexte (et un autre thread) pour télécharger les données de votre service Web. Une fois que tout est là, parcourez les processus de synchronisation / importation recommandés ci-dessous, puis actualisez votre interface utilisateur une fois que tout est correctement importé. Si les choses se gâtent lors de l’access au réseau, il suffit de restaurer les modifications dans le contexte non-interface utilisateur. C’est un tas de travail, mais je pense que c’est la meilleure façon de l’approcher.

Données de base: importation efficace des données

Données de base: gestion du changement

Données de base: multi-threading avec données de base

Vous avez besoin d’une fonction de rappel qui s’exécutera sur l’autre thread (celui où l’interaction du serveur se produit), puis placez les informations de code de résultat / d’erreur comme des données semi-globales qui seront vérifiées périodiquement par le thread d’interface utilisateur. Assurez-vous que le nombre qui sert de drapeau est atomique ou que vous allez avoir une condition de concurrence – dites que si votre réponse d’erreur est de 32 octets, vous avez besoin d’un int (qui devrait avoir un access atomique) dans l’état off / false / not-ready jusqu’à ce que votre plus grand bloc de données ait été écrit et que vous écrivez ensuite “true” pour inverser le commutateur pour ainsi dire.

Pour la sauvegarde corrélée du côté client, vous devez soit conserver ces données et ne pas les enregistrer jusqu’à ce que vous obteniez un message correct du serveur. Assurez-vous d’avoir une option de restauration – disons qu’un moyen de supprimer le serveur a échoué.

Attention, il ne sera jamais sûr à 100% sauf si vous effectuez une procédure de validation en deux phases (la sauvegarde ou la suppression du client peut échouer après le signal du serveur), mais cela vous coûtera au moins 2 voyages ( peut vous coûter 4 si votre seule option d’annulation est supprimée).

Idéalement, vous devriez faire toute la version bloquante de l’opération sur un thread séparé, mais vous auriez besoin de 4.0 pour cela.