WatchKit Extension et core data partagé
Bonjour à tous,
Je développe une application pour iPhone et Apple Watch.
J'utilise Core Data pour partager mon modele de données et j'ai appliqué les recommandations de la documentation.
A savoir, l'ajout d'un d'un "app group" et d'un "embedded framework" qui contient mon code métier core data.
Cela fonctionne bien tant que je fais uniquement de la lecture seule de ma base de données.
J'ai déplacé le code Template Core Data généré par Xcode dans une classe de mon Embedded framework et à l'image du fonctionnement avec l'AppDelegate, j'accede au ManagedObjectContext via un singleton.
public class CoreDataStack : NSObject
{
public static let sharedInstance = CoreDataStack()
func contextDidSave(notification:NSNotification)
{
self.managedObjectContext?.mergeChangesFromContextDidSaveNotification(notification)
}
// MARK: - Core Data stack
...
...
// j'utilise bien containerURLForSecurityApplicationGroupIdentifier(...) pour l'URL du store
...
}
Le souci est que j'ai deux instances de NSManagedObjectContext différentes pour l'App iOS et l'extension et que je ne parviens pas à les merger.
-> La modification puis sauvegarde d'un objet dans un contexte n'est pas repercuté dans l'autre contexte.
Je refais des requêtes Fetch ou des refresh de l'objet pour éviter une corruption des données mais les contextes étant différents m'on objet est tout de même non synchronisé.
Je partage bien le meme store car lorsque je recompile et run à nouveau mon extension et mon App iOS, l'objet est au même état dans les 2 target....
Je sèche sur le sujet et j'aimerais savoir si l'un de vous a dejà rencontré ce genre de problème.
Merci à tous
Réponses
Ce sont deux process différents (App iOS et WatchApp) donc chacun ses objets en memoire, tu ne pourras pas avoir la même instance (le même pointeur memoire) partagée par les eux vu que ce sont deux process distincts vivant chacun dans son espace.
Par contre puisque CoreData a un StoreCoordinator il est déjà pensé pour que plusieurs process accèdent au même fichier sqlite de façon concurrente. Chaque process se basera donc sur le même sqlite mais chacun créera ses propres MOC avant de fusionner le tout dans la base sur disque. Si tu veux écrire des données dans ta base CoreData depuis ton app et qu'elles soient visibles ensuite depuis ta Watch faut faire remonter les données sauvées jusqu'au fichier sqlite (jusqu'au NSPersistantStoreCoordinator utilisé par le process de ton app) puis aller demander au MOC directement lié au PSC côté Watch (et pas à un de ses childContext qui aurait été crée avant l'addition des données donc) de requêter les données.
En pratique c'est plutôt deconseillé d'avoir ce modèle d'architecture, car tu vas devoir penser à pas mal de cas tricky quand tu as des accès concurrents entre les 2 process et tout gérer ces cas à la main etc. C'est possible techniquement mais c'est galère.
Il vaut mieux centraliser le référentiel de données côté App iOS, et utiliser les mécanismes dédiés et mis à disposition d'Apple dans le SDK WatchKit pour faire communiquer ta WatchApp et ton Appli iOS ("openParentApplication:reply:"). Les AppGroups et le SharedContainer c'est plutôt pour partager des documents (et encore il faut penser à utiliser NSFileCoordinator pour gérer les accès concurrents aux documents de la sandbox partagée dans ce cas) qu'une base CoreData qui serait ouverte et mise à jour en live par les deux car effectivement là tu n'auras aucune synchro live.
Merci Ali,
En gros, remonter les données dans la base sqlite cela revient à faire un simple context.save() c'est bien ça ?
C'est le point qui fait défaut chez moi du coup. je vais apporter la modification
A terme je devrais surement éviter cette architecture effectivement, pourtant j'ai développé toute l'app iOS sur ce modele en pensant me "faciliter" l'integration future de l'extension... </p>
Et justement j'utilise openParentApplication:reply: pour communiquer de la watch vers l'App mais comment faire l'inverse ? ca ne me parait pa clair, faut-il utiliser les notifications ?
Est-ce que tu as rafraichi ton contexte ?
CoreData dispose de toute la mécanique nécessaire en interne pour faire de l'accès mutli-process. La seule chose qui n'est pas embarqué d'origine c'est la capacité de remonter le changement.
Aucun mécanisme de CoreData ne permet nativement de remonter des fichiers vers le contexte. Pour ça il faut faire des merge en disposant des ID inséré, modifié et supprimés. Ou si on peut se le permettre, recharger complètement le contexte (et perdre les modifications et le cache).
Je rafraà®chi mon objet depuis mon contexte.
Je n'ai pas vu de méthode pour refresh le contexte directement, je tente plutot un merge des contextes apres une sauvegarde.
Soit tu fait un merge en effet (méthode propre) soit tu peux faire un reset sur le moc (en faisant attention de bien virer toutes les références vers les NSManagedObject au préalable).
Je reste persuadé pour avoir deja plusieurs fois expérimenté avec des projets Watch au boulot que même si c'est possible techniquement de partager le sqlite et que chaque process y accède en parallèle, ça n'est à mon avis pas la meilleure solution, et que laisser seule ton app gérer ta base est plus clean et plus simple. Entre les [NSUserDefaults suiteNamed:...], le openParentApplication:reply:, et HandsOff, ca te laisse suffisamment de moyens de communiquer et centraliser le code et la gestion de CoreData à un seul endroit côté App non ?
Après si tu tiens à garder un sqlite partagé et y accéder de tes 2 process " c'est possible après tout c'est juste qu'il faut bien comprendre les conséquences et architecturer en conséquence " tu peux toujours remonter à ton appli Watch que le contenu de la base à changé côté App " et donc qu'il faut que la WatchApp recharge tout le MOC racine (lié au PSC) pour les relire " en utilisant le NSUserDefaults partagé dans ton AppGroup ou sinon via HandsOff, ça se fait en juste 2-3 lignes de code max pour remonter l'info au final, mais bon...
Du coup, j'ai appliqué les conseils d'Ali en déléguant l'écriture dans Core Data à mon app iOS.
Et j'ai appliqué les conseils de Yoann en effectuant un reset du contexte dans l'extension avant de faire des lectures dans core data.
Cela fonctionne bien dans la mesure où coté watch je n'exploite qu'un seul objet core data et la gestion des références apres reset est tres simple.
Après coup je suis tout à fait de ton avis Ali quant à centraliser la gestion core data côté App.
Mais alors quel est ton avis sur le "embedded farmework" qui permet de mutualiser du code entre ton app et extension, cela apparait pour moi comme une solution tres interessante
Perso, je ne suis pas tout à fait d'accord...
L'extension et l'app hôte peuvent très bien partager le store dans l'AppGroup.
Ca sert à rien de demander à l'extension Watch d'aller sollicitier l'app hôte pour aller taper dans le store... Je vois pas l'intérêt.
Oui voilà , j'ai laissé mon store et mon code métier dans le container partagé mais je n'y fais que de la lecture depuis la watch.
Pour l'écriture je délègue à l'App iOS
Comme dit plus haut c'est possible de partager le store dans l'AppGroup, faut juste avoir conscience des contraintes, en particulier que l'App et la Watch ne seront jamais automatiquement synchronisées (surtout si tu as une architecture de MOC avec des childContext partout comme c'est souvent le cas par exemple quand on fait du parsing de WS où on a un child le temps du parse etc) tant que tu n'auras pas tout remonté jusqu'au dernier niveau du PSC après le save et d'un côté et tout reloadé depuis le fichier de l'autre côté. Si tu as deja des childContext basés sur des MOC existants, tous t'en débarrasser pour tout reconstruire from scratch ou bien faire des reset en cascade partout c'est vite lourd. Pas infaisable mais lourd. Alors qu'un référentiel unique...
MOC, ChildContext, PSC, c'est du chinois pour moi.
Ca veut dire quoi iPhone et Watch synchro pour toi ?
Tu veux gérer le gars qui va lancer à la fois l'app sur la watch ET sur le phone et qui va s'amuser à comparer si c'est synchro ?
En fait, j'ai du mal à imaginer un cas où ça pourrait arriver... Un cas où la watch et l'iPhone voudraient écrire/lire le meme store, en meme temps...
background notif sur l'iPhone qui lance le DL de contenu.
Mais dans tous les cas, ChildContext ou pas, si ton principe d'usage de CoreData est correct, tant que ce n'est pas dans le PSC et sur le disque, c'est que les données ne sont pas finales. Donc ne doivent pas être envoyé sur la montre.
Heu je comprends pas trop là ... tu parles de CoreData et tu fais des justifications sur "à mon avis c'est mieux de le mettre dans l'AppGroup" etc... mais tu ne connais pas les bases de fonctionnement de CoreData comme ManagedObjectContext, childContext (ou plutôt contentWithParent) et PersistentStoreCoordinator ?! Du coup comment tu peux arriver à comprendre les justifications de comment gérer ça pour partager un store commun App/Watch dans un AppGroup ?
iPhone et Watch synchro sinon pour moi ça veut dire... bah ce que demande starmendo dans son post initial. C'est à dire espérer que quand on écrit un truc dans CoreData (pour être plus précis : quand on save la modification d'un ManagedObject dans son MOC) du côté de l'App, on puisse directement faire un Fetch du côté du code de la Watch et espérer récupérer le ManagedObject qu'on vient de sauver dans l'App.
Or ça, pour les raisons déjà expliquées plus haut (instances de PSC séparées, etc) ce n'est pas possible immédiatement ou alors ça nécessite de faire des "reset" sur tous tes MOC pour les forcer à recharger les donnée du fichier sqlite... bref ce que j'explique depuis le début.
En gros ce que je veux dire par le fait que l'App et la Watch n'auront pas un contexte CoreData synchronisé c'est justement ça, c'est que si tu save un ManagedObject du côté de l'App tu ne le verras pas en faisant directement un Fetch du côté de la Watch, pour toutes les raisons expliquées plus haut.
Par principe puisque c'est une Extension, comme toutes les extensions, oui les données peuvent être accédées en même temps. Donc oui c'est un cas + que fréquent. Pas parce que le gars va regarder en même temps sa montre et son iPhone, mais parce que quand le gars regarde ça Watch et que ça réveille la WatchApp, l'App iPhone va souvent déjà être lancée en background (voire exécuter du code également, si tu fais usage du "openParentApplication:reply:"). Et parce que si tu ne gères pas ce cas là , les aspects synchro d'état et de données c'est une horreur à gérer (demande à muquaddar ).
Genre imagine tu lances ton app iPhone, elle crée des objets CoreData, les save dans le defaultContext (le MOC principal dont elle se sert), mais pas jusqu'au PSC (donc c'est pas encore sur disque), ou peut-être qu'elle fait une requête réseau en tâche de fond et qu'elle va plus tard, au retour de la requête réseau, stocker des données dans le MOC principal.
Toi pendant ce temps là t'as repassé l'app iOS en background (mais la requête continue son chemin) et 2mn plus tard tu regardes ta Watch (parce que tu te dis "tiens c'était quoi déjà l'info que je viens de regarder dans mon iPhone ? Vite un coup d'oeil rapide pour la relire sur ma Watch"), qui elle va pas du tout avoir les données à jour.
Avoir 2 process (App et Watch) qui lisent une même base CoreData/sqlite en même temps, c'est le même risque que d'avoir 2 utilisateurs qui éditent en même temps un document Word sur le réseau. Le premier qui sauve ses modifications va se les faire écraser par le 2ème qui aura fait d'autres modifications potentiellement incompatibles (sauf si ce 2ème pense à réouvrir le document au dernier moment avant de réappliquer les modifs sur la version du 1er == sauf si tu fais un "reset" sur le MOC pour annuler tes modifs locales et recharger les données du disque).
J'ai pas dit "A mon avis c'est mieux". J'ai dis "je vois pas l'intérêt de ne pas le faire". C'est pas pareil.
Pour ce qui est de MOC et PSC, maintenant que tu précises, oui, je vois mieux. Mais effectivement, CoreData, je l'utilise à travers MagicalRecord donc tout ça est masqué et je ne m'en préoccupe guère.
Mais ok, je comprends la problématique pour des applications complexes. Cependant, bien souvent, en tous cas, de ce que j'en sais et/ou vois, une extension reste une extension. Donc reste ultra simpliste.
Après, effectivement, ton exemple est parlant. Donc ok, j'adopterai ce modèle à l'avenir.
De mon côté, mes 2 apps watch partagent des données mais il n'y a pas cette notion de concurrence ; enfin si, il y a concurrence mais pas de pb de synchro...