Les notifications en Objective-C - Partie 1
Les notifications sont un mécanisme qui permet de communiquer des informations liées à un événement, et ce, au sein d’une même application ou entre différentes applications. Sans que vous le sachiez, ce mécanisme est systématiquement utilisé dans toutes les applications Cocoa, par exemple lorsque vous fermez une fenêtre, lancez une application, sélectionnez une ligne dans un tableau,… une notification est à chaque fois envoyée. Les informations transmises par la notification sont fonction de la tâche effectuée. Dans les exemples donnés, il s’agira d’un pointeur vers l’objet NSWindow qui a été fermé; le nom de l’application, le chemin pour y accéder, l’identifiant de processus de l’application… dans le cas de l’application qui vient d’être lancée; et dans le cas du changement de sélection dans un tableau, aucune (on informe juste que la sélection a changé, rien de plus). Dans beaucoup de programmes, vous pouvez remarquer que lorsque vous modifiez les préférences, les changements se font en temps réel, là aussi c’est un envoi de notification qui permet à l’application de mettre à jour ce qu’il faut. Il est aussi possible de créer ses propres notifications, qui contiendront alors l’information que vous souhaitez.
Lorsqu’il faut transmettre de l’information entre différents objets, l’usage des notifications est particulièrement indiqué dans les cas suivants :
- on ne connaît pas à priori les objets qui pourront être concerné par un événement, ni leur nombre ;
- les objets concernés par un événement sont exécutés dans une autre tâche ;
- il n’est pas nécessaire de renvoyer une information, l’important étant de faire savoir qu’un événement a eu lieu, mais non de fournir, par exemple, le résultat d’un calcul.
Pour mettre en place les notifications au sein d’une application plusieurs objets sont impliqués : en premier lieu, un objet envoyeur, qui envoie une notification (un objet NSNotification), lorsqu’un événement se produit. Ensuite, le second objet impliqué dans la chaîne est le centre de notification (un objet NSNotificationCenter), dont la fonction est d’une part de recevoir les notifications provenant des différents objets qui en envoient, et d’autre part retransmettre la notification aux objets enregistrés, qui sont, vous l’aurez deviné, les destinataires finaux de la notification. Il existe sur une machine plusieurs centres de notifications, dont la portée est clairement définie :
- au sein d’une application, il y a un centre de notification par tâche. Si vous ne savez pas ce qu’est une tâche, pas de panique, votre application est à coup sûr monotâche, il n’y aura donc qu’un seul centre de notification pour l’application ;
- un pour l’espace de travail, qui regroupe les tâches utilisant l’Application Kit de Cocoa, c’est-à-dire, en pratique, toutes celles qui ont une interface graphique ;
- un dernier pour toutes les applications écrites en utilisant le framework Foundation, qui incluent en plus des outils graphiques certains outils UNIX fonctionnant en ligne de commande et programmés en Objective-C.
Cet article sera divisé en deux. Le premier article introduira l’usage des notifications : il y sera expliqué comment enregistrer un objet, et comment poster une notification. Sa lecture suffit pour la plupart des cas. Dans le deuxième article, nous aborderons l’organisation de l’arrivée des notifications au centre de notification, avec notamment les problèmes liés à l’envoi d’une notification livrée à un objet temporairement indisponible.
Enregistrer un objet au centre de notification
Tout objet peut être enregistré auprès du centre de notification pour recevoir des notifications. Avant d’enregistrer un objet, plusieurs choses sont nécessaires : connaître l’identifiant de la notification que l’objet doit recevoir, une méthode à exécuter lorsque la notification arrivera, et éventuellement, le type d’objet qu’on s’attend à recevoir. La méthode à exécuter devra avoir un unique argument, de type NSNotification. Une fois que ces conditions sont remplies, on peut s’enregistrer auprès du centre de notification, par la méthode addObserver:selector:name:object:.Examinons les différents arguments :
- Le premier argument est l’objet dans lequel se trouve la méthode à exécuter lorsque la notification arrive, qui sera dans la plupart des cas l’objet qui s’enregistre auprès du centre de notification, c’est-à-dire self.
- Le deuxième argument nécessaire est le sélecteur qui désignera la méthode à exécuter,
- Le troisième argument, name, est le nom de la notification, ce qui permettra au centre de notification de ne transférer que les notifications utiles à l’objet enregistré. Si on attribue nil à cet argument, il recevra toutes les notifications du centre de notification pour lequel il s’est inscrit. Il doit être de type NSString.
- Le quatrième argument, object, est utile s’il faut, pour une notification ayant le même nom, employer des méthodes différentes en fonction de l’objet reçu, ce qui la plupart du temps ne sera pas le cas, on lui attribuera donc nil.
Illustrons ceci par un petit exemple : ouvrez l’exemple UserDefaults (situé dans /Developer/Examples/Appkit si vous avez installé les exemples). L’objectif de ce programme est d’illustrer l’usage de la classe NSUserDefaults, il se compose d’un champ, dans lequel on tape une expression et d’un bouton, qui permet l’enregistrement de l’expression. L’expression est rétablie dans le champ au prochain redémarrage de l’application. Nous allons y apporter les modifications suivantes : lorsqu’on change le contenu du champ texte, un point noir doit s’inscrire dans le bouton fermer de la fenêtre (comme dans TextEdit lorsque vous modifiez un document); et lorsque les préférences sont sauvées, le point noir doit disparaître (comme dans TextEdit lorsque vous …sauvez un document). Pour placer le point noir, il suffit d’envoyer le message setDocumentEdited: avec comme argument YES. Pour le faire disparaître l’argument doit être NO.
Dans l’en-tête (Controller.h), ajoutez simplement :
- (void)textHasChanged:(NSNotification *)notification;
Dans l’implémentation (Controller.m), ajoutez :
- (void)textHasChanged:(NSNotification *)notification {
[myWindow setDocumentEdited :YES];
}
Rajoutez la ligne suivante dans l’implémentation de la méthode applicationDidFinishLaunching:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textHasChanged:) name:NSControlTextDidChangeNotification object:nil];
Rajoutez la ligne suivante dans l’implémentation de la méthode textFieldAction:
[myWindow setDocumentEdited :NO];
Le message [NSNotificationCenter defaultCenter] permet d’accéder au centre de notification de l’application. Le nom de la notification, NSControlTextDidChangeNotification, peut être trouvé dans l’aide. Il s’agit d’une notification envoyée systématiquement par NSControl (qui est une des classes dont NSTextField hérite) lorsque le texte a changé. Donc, une fois que la notification arrive au centre de notification, elle est renvoyée vers Controller, qui exécutera alors la méthode textHasChanged:, qui placera le fameux point noir. Il est enlevé en cliquant sur le bouton Record.
On peut un peu perfectionner l’exemple. Lorsque les préférences sont enregistrées, le titre de la fenêtre doit devenir l’expression sauvegardée. Pour faire ça, on va utiliser une autre notification envoyée par NSUserDefaults lorsque les préférences ont été modifiées. Le nom de cette notification est NSUserDefaultsDidChangeNotification. Les notifications sont capables de transporter des informations, qui seront dans ce cas les préférences elles-mêmes. Pour y accéder, il suffit d’envoyer le message object à la notification. L’objet renvoyé est une instance de NSUserDefaults. Pour extraire la valeur qui nous intéresse, il suffit de faire comme pour n’importe quelle instance de NSUserDefaults, c’est-à-dire envoyer un message stringForKey:. Voici le code :
Dans l’interface, rajoutez :
- (void)prefsHaveChanged:(NSNotification *)notification;
Dans l’implémentation, rajoutez ceci :
- (void)prefsHaveChanged:(NSNotification *)notification {
[myWindow setTitle:[[notification object] stringForKey:myKey]];
[myWindow setDocumentEdited:NO];
}
Enfin rajoutez l’observateur dans applicationDidFinishLaunching:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prefsHaveChanged:) name: NSUserDefaultsDidChangeNotification object:nil];
Retirez la ligne [myWindow setDocumentEdited:NO]; dans l’implémentation de textFieldAction, cette ligne y est maintenant inutile, puisque le point noir disparaît lorsque la notification NSUserDefaultsDidChangeNotification est reçue.
Je vous invite à parcourir un peu l’aide de l’Application Kit, pour voir quels sont les notifications qui existent pour les différentes classes. Vous remarquerez que l’aide mentionne souvent un certain dictionnaire userInfo. Il s’agit d’un NSDictionary envoyé avec la notification, qui contient quelques informations complémentaires qui peuvent faciliter la vie au programmeur. Prenons par exemple une notification de NSWorkspace, NSWorkspaceDidLaunchApplicationNotification, envoyée lorsqu’une application vient d’être lancée : le userInfo contient les “coordonnées” de l’application qui vient d’être lancée, c’est-à-dire le nom de l’application, son chemin d’accès, son numéro de processus, la version courte et la version longue. Pour y accéder, il faut envoyer un message userInfo à l’objet NSNotification, de la même façon que le message object est envoyé pour recevoir l’objet “joint” à la notification. Dans le cas de NSControlTextDidChangeNotification, le dictionnaire ne contient qu’une seule entrée à la clé NSFieldEditor, qui renseigne un objet de type NSTextView, qui correspond au champ texte en cours d’édition.
Il va de soi que cet exemple pourrait être fait sans passer par les notifications. Sachez que si l’objet qui envoie la notification et l’objet observateur ne sont pas placés dans le même fichier .nib et/ou n’auraient aucun lien entre eux, ça fonctionnerait tout aussi bien, tant qu’on reste dans la même tâche.
Il est aussi possible de s’enregistrer auprès d’autres centres de notification existants sur l’ordinateur. Pour s’inscrire auprès du centre de notification de l’espace de travail, qui désert toutes les applications graphiques, on utilisera le message [[NSWorkspace sharedWorkSpace] notificationCenter]. Son fonctionnement est tout à fait similaire à ce qui a été décrit. Les notifications typiques envoyées par ce centre sont décrites dans la feuille d’aide NSWorkspace. L’objet associé à une notification envoyée via ce centre est toujours l’instance partagée de NSWorkspace qu’on désigne par le message [NSWorkspace sharedWorkSpace].
Le troisième centre de notification accessible à une application est celui du framework Foundation. Son fonctionnement est également similaire à celui de l’application, à une différence près : l’objet envoyé par la notification ne peut être que du type NSString. Cette restriction est liée au fait que l’objet qui envoie la notification et celui qui la reçoit sont dans des tâches bien distinctes, envoyer un pointeur vers un objet serait alors problématique. Pour désigner ce centre de notification, le message à utiliser est : [NSDistributedNotificationCenter defaultCenter].
Limiter les notifications pour un seul objet
L’exemple montre aussi très vite ses limites : rajoutez simplement un second champ texte dans la fenêtre, le point noir sera placé lorsque ce nouveau champ est édité, alors qu’il n’a rien à voir avec les préférences. C’est normal, car chaque NSTextField envoie d’office ce type de notification lorsqu’il est modifié.
Si vous désirez avoir une réponse aux événements d’un objet bien précis, plusieurs mécanismes sont possibles : filtrer les objets au niveau de l’observateur, jouer sur les propriétés de l’objet envoyé ou jouer sur la délégation pour envoyer des notifications personnalisées. Le choix du mécanisme devra s’effectuer au cas par cas.
Le premier consiste à spécifier lors de la création de l’observateur à quel objet doit se limiter la notification, mais il faut alors désigner cet objet par un pointeur : ce qui implique que le destinataire de la notification puisse avoir accès à cet objet. Ce cas est intéressant lorsque le destinataire et l’envoyeur de la notification utilisent le même contrôleur, puisqu’une connexion entre un objet et un contrôleur est facile à établir, avec InterfaceBuilder par exemple. Dans l’exemple donné, pour créer l’observateur, il faut alors plutôt utiliser le message :
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textHasChanged:) name:NSControlTextDidChangeNotification object:myTextField];
La seconde possibilité, consiste à rechercher dans les propriétés de l’objet envoyé avec la notification ce qui pourrait identifier clairement l’objet, comme par exemple son tag, si un tag différent a été défini pour chaque objet. Cette situation est intéressante lorsque l’objet destinataire n’a pas accès à l’objet envoyeur, le filtrage se faisant alors en jouant sur des propriétés facilement éditables dans InterfaceBuilder, comme le tag. Dans l’exemple donné, on pourrait par exemple avoir le code suivant pour la méthode textHasChanged, si on attribue 1 au tag du second NSTextField:
- (void)textHasChanged:(NSNotification *)notification {
if ([[notification object] tag] != 1) [myWindow setDocumentEdited:YES];
}
La troisième méthode consiste à utiliser la délégation. On programmera ensuite le délégué pour qu’il envoie alors une notification personnalisée. Ce cas servira d’exemple pour illustrer l’envoi d’une notification, détaillé dans le point suivant.
Créer ses propres notifications
Il est possible de créer ses propres notifications. Pour ce faire, vous pouvez soit créer un objet NSNotification, et l’envoyer via le NSNotificationCenter, ou bien utiliser une méthode de commodité, ce que nous recommandons. Pour poster des notifications à un NSNotificationCenter, on peut utiliser deux méthodes de commodité : postNotificationName:object:userInfo: ou postNotificationName:object:. Les arguments sont :
- le nom de notification, de type NSString;
- l’objet envoyé lors de la notification, il peut s’agir de n’importe quel type d’objet qui est exécuté au sein de la tâche même;
- un NSDictionary qui contient des informations additionnelles utiles pour l’objet qui recevra la notification, utiliser la deuxième méthode ou attribuer nil à cet argument revient au même.
Si on désire envoyer des notifications à un NSDistributedNotificationCenter, la procédure est la même que pour le NSNotificationCenter, mis à part que l’objet envoyé ne peut être que de type NSString. Si cependant, on désire réellement envoyer un autre type d’objet qu’une NSString, il suffit simplement de placer cet objet dans un dictionnaire et d’envoyer ce dictionnaire en le plaçant comme argument pour userInfo. Il y a également un paramètre optionnel (deliverImmediately:), que nous n’aborderons pas ici de manière approfondie, puisque son usage demande des prérecquis qui se trouveront dans la deuxième partie de l’article.
En qui concerne l’exemple, nous allons faire d’une pierre deux coups : l’exemple montrera bien sûr l’envoi d’une notification, et aussi l’utilisation de la délégation pour envoyer des notifications. On va reprendre l’exemple pris tout au long de cet article. La valeur du second NSTextField sera directement sauvegardée dans les préférences en cours de frappe. La première étape consiste à déléguer cet objet à Controller. Pour ce faire, il suffit de tirer une connexion de l’objet à déléguer, le second NSTextField, vers l’objet délégué, qui est dans notre cas le contrôleur, de choisir delegate dans la liste qui apparaît dans l’inspecteur puis de valider la création de la connexion. Dans l’implémentation du contrôleur, mettez le code suivant :
Rajoutez l’observateur :
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(savePrefs:) name:@"une notification" object:nil];
L’implémentation de la méthode à utiliser lors du changement (n’oubliez pas la déclaration dans l’interface) :
- (void)savePrefs:(NSNotification *)notification {
[[NSUserDefaults standardUserDefaults] setObject:[notification object] forKey:myKey];
}
//le code propre à la délégation
- (void) controlTextDidChange:(NSNotification *) aNotification {
[[NSNotificationCenter defaultCenter] postNotificationName:@"une notification"
object:[[aNotification object] stringValue]];
}
La première partie de ce code doit maintenant être comprise, je n’y reviendrai donc pas. Pour le code propre à la délégation, il faut d’abord trouver la méthode pour laquelle on désire déléguer. L’aide des classes qui peuvent être déléguée donne les différentes méthodes implémentables dans l’objet délégué, sous le titre Methods implemented by the delegate, juste à la suite de la description des méthodes d’instance. Une fois qu’on a le prototype, il suffit de rajouter la méthode correspondante dans l’implémentation de l’objet délégué. Nul besoin de rajouter une déclaration dans l’en-tête.
Pour l’envoi de la notification, il faut d’abord spécifier le centre de notification auquel va être envoyé la notification : il suffit de le désigner par les messages déjà mentionnés : [NSNotificationCenter defaultCenter] pour les notifications à destination de la tâche dans laquelle se trouve l’objet, et [NSDistributedNotificationCenter defaultCenter] pour toutes les applications. En ce qui concerne l’objet envoyé par la notification, vous remarquerez qu’il n’est pas nécessaire d’envoyer un objet “graphique” entier, comme c’est le cas dans beaucoup des notifications de Cocoa. On peut en effet n’envoyer que l’information qu’on sait utile, dans la mesure où on connaît le rôle précis de la notification, ce qui n’a pas été le cas des développeurs qui ont conçu le framework Cocoa. Pour l’objet toujours, n’oubliez pas que si vous utilisez un NSDistributedNotificationCenter, il ne peut être que de type NSString.
Avant de terminer avec l’envoi d’une notification, je voudrais quand même attirer l’attention sur un aspect parfois sensible : la confidentialité des données. Si vous utilisez le centre de notification de la tâche, pas de problèmes de confidentialité, la notification étant confinée à la tâche, il ne peut donc avoir de fuites indésirables. Si par contre vous employez le NSDistributedNotificationCenter, toute application pourra recevoir la notification et donc les infos qui lui sont attachées, puisque, je le rappelle, il suffit d’attribuer nil aux arguments name et object lors de la déclaration de l’observateur pour recevoir toutes les notifications envoyées via ces centres. Si vous désirez passer des informations confidentielles entre deux applications distinctes, la solution à utiliser est plus complexe : il faut d’abord envoyer une notification pour informer qu’il y a un changement, de façon à ce que l’application cible sache qu’elle doit se mettre à jour. Pour la mise à jour à proprement parler, il faudra qu’elle passe par exemple par les objets distribués ou bien qu’elle lise directement le fichier mis à jour.
Le cas du centre de notification associé au NSWorkspace
J’ai mentionné dans au début de l’article un troisième centre de notification, qui n’a même pas été cité dans lepoint traitant de l’envoi de notification : le centre de notification associé au NSWorkspace. La raison est en fait assez simple : on sait pas envoyer de notifications via ce centre. L’envoi est pour ce centre réservé au système.
Désenregistrer un objet
Si vous libérez un objet observateur, comme par exemple lorsque vous quittez une application dont un des objets s’est enregistré au NSDistributedNotificationCenter, il vaut mieux signaler cette disparition au centre de notification. On le signale en lui envoyant un message removeObserver:, qui prend comme argument l’objet qu’il faut désenregistrer, c’est-à-dire l’objet qui a été placé comme premier argument dans la déclaration de l’observateur (self dans la majorité des cas). Par exemple, pour un objet qui s’enregistré au NSDistributedNotififcationCenter, il faudrait rajouter le code suivant dans l’implémentation de dealloc:
[[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
Cette méthode désenregistrera tous les observateurs rattachés à l’objet. Si vous désirez ne désenregistrer que des observateurs choisis suivant certains critères, parce qu’ils recevaient des notifications d’un objet qui a été libéré par exemple, il faut utiliser la méthode removeObserver:name:objet:, où le premier argument est l’objet enregistré, le second le nom de la notification et le troisième l’objet pour lequel il faut faire suivre les notifications. Comme pour les déclarations d’observateurs, vous pouvez attribuer nil à un des arguments, ce qui aura pour effet de ne pas ajouter l’argument en question comme critère de sélection pour le désenregistrement. Si dans notre exemple, nous ne voulions plus qu’il y aie un observateur pour la notification personnalisée qui a pour nom @”une notification”, il aurait fallu rajouter la ligne :
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"une notification" objetct:nil];
Il y a ici une subtilité : si vous attribuez nil au premier argument, tous les observateurs du centre de notification choisi seront supprimés (donc même ceux qui ont été créés dans d’autres objets !), avec un tri possible en fonction du nom et/ou de l’objet. Donc par exemple, si vous avez créé une série d’observateurs pour l’objet unObjet, et que vous le libérez, vous enverrez le message suivant au centre de notification :
[[NSNotificationCenter defaultCenter] removeObserver:nil name:nil object:unObjet];
Résumé
Pour désigner un centre de notification:
[NSNotificationCenter defaultCenter] // centre de notification de la tâche; [[NSWorkspace sharedWorkSpace] notificationCenter] // centre de notification des programmes utilisant l'Application Kit; [NSDistributedNotificationCenter defaultCenter] // centre de notification des programmes utilisant Foundation.
Pour enregistrer un objet au centre de notification:
[unCentreDeNotification addObserver:unObservateur selector:unSélecteur name:unNom object:unObjet];
où unObservateur est l’objet enregistré, unSélecteur est le sélecteur de la méthode à exécuter lors de la réception de la notification, unNom est le nom de la notification (si on attribue nil, toutes les notifications de unCentreDeNotification seront reçues) et unObjet permet de choisir l’objet pour lequel on désire recevoir les notifications (si on attribue nil, aucun filtrage quand à l’objet ne sera fait).
Il faut également rajouter la méthode qui va être exécutée lors de la réception de la notification. Cette méthode doit avoir un seul argument de type NSNotification.
Pour poster une notification
[unCentreDeNotification postNotificationName:unNom object:unObjet;
ou bien;
[unCentreDeNotification postNotificationName:unNom object:unObjet userInfo:unDictionnaire];
où unNom est le nom de la notification, unObjet est l’objet qui est envoyé avec la notification (ne peut être que de type NSString pour un NSDistributedNotificationCenter) et unDictionnaire (optionnel) est NSDictionary qui contient des informations additionnelles qui peuvent être utiles à l’objet destinataire. On n’envoie pas de notifications via le centre associé au NSWorkpace.
Pour désenregistrer un objet
[unCentreDeNotification removeObserver:unObservateur];
où unObservateur est l’objet à désenregistrer.
[unCentreDeNotification removeObserver:unObservateur name:unNom object:unObjet];
où unObservateur est l’objet à désenregistrer, unNom est le nom de la notification pour laquelle il faut supprimer un observateur et unObjet est l’objet pour lequel les observateurs doivent être supprimés. Attribuer nil à un argument permet de se servir de l’argument comme critère de sélection pour la suppression. Si on attribue nil à la place de unObservateur, les observateurs créés par d’autres objets qui utilisent le même centre de notification peuvent également être supprimés.
Fin de la première partie
Normalement cette première partie devra pouvoir vous suffire pour la grande majorité des cas. La deuxième partie de cet article vous sera utile si vous faites un usage important du centre de notification distribué, car certains problèmes non traités ici peuvent apparaître, comme l’envoi d’une notification à un objet déjà occupé à exécuter une méthode, qui pourra, si peu de précautions sont prises, “paralyser” l’objet envoyeur.
Bon code !
© Juillet 2003 Renaud pour Project:Omega
Chargement
Commentaires récents