Comment puis-je savoir si un objet a un observateur de valeur clé attaché

142

si vous dites à un objet objectif c de removeObservers: pour un chemin de clé et que le chemin de clé n'a pas été enregistré, il craque les tristes. comme -

'Impossible de supprimer un observateur pour le chemin de clé "theKeyPath" car il n'est pas enregistré en tant qu'observateur.'

y a-t-il un moyen de déterminer si un objet a un observateur enregistré, donc je peux le faire

if (object has observer){
  remove observer
}
else{
  go on my merry way
}
Aran Mulholland
la source
Je suis entré dans ce scénario en mettant à jour une ancienne application sur iOS 8 où un contrôleur de vue était désalloué et en lançant l'exception «Impossible de supprimer». Je pensais qu'en appelant addObserver:dans viewWillAppear:et en conséquence removeObserver:dans viewWillDisappear:, les appels ont été jumelés correctement. Je dois faire une solution rapide, donc je vais mettre en œuvre la solution try-catch et laisser un commentaire pour enquêter davantage sur la cause.
bneely
Je traite juste de quelque chose de similaire et je vois que je dois examiner ma conception plus en profondeur et l'ajuster afin de ne plus avoir à supprimer l'observateur.
Bogdan
l'utilisation d'une valeur booléenne comme suggéré dans cette réponse a fonctionné le mieux pour moi: stackoverflow.com/a/37641685/4833705
Lance Samaria

Réponses:

315

Mettez un crochet autour de votre appel removeObserver

@try{
   [someObject removeObserver:someObserver forKeyPath:somePath];
}@catch(id anException){
   //do nothing, obviously it wasn't attached because an exception was thrown
}
Adam
la source
12
1+ Bonne réponse, a fonctionné pour moi et je suis d'accord avec votre diatribe avant qu'il ne soit édité.
Robert
25
voté pour la diatribe supprimée avec laquelle je serais probablement d'accord.
Ben Gotow
12
N'y a-t-il pas ici une autre solution élégante? celui-ci prend au moins 2 ms par utilisation ... imaginez-le dans une tableviewcell
João Nunes
19
Évalué parce que vous omettez de dire que cela n'est pas sûr pour le code de production et risque d'échouer à tout moment. Lever des exceptions via le code du framework n'est pas une option dans Cocoa.
Nikolai Ruhe
6
Comment utiliser ce code dans swift 2.1. do {try self.playerItem? .removeObserver (self, forKeyPath: "status")} catch let error as NSError {print (error.localizedDescription)} obtenant un avertissement.
Vipulk617
37

La vraie question est de savoir pourquoi vous ne savez pas si vous l'observez ou non.

Si vous faites cela dans la classe de l'objet observé, arrêtez. Quoi qu'il en soit, il s'attend à le continuer à l'observer. Si vous coupez les notifications de l'observateur à son insu, attendez-vous à ce que les choses se cassent; plus spécifiquement, attendez-vous à ce que l'état de l'observateur devienne périmé car il ne reçoit pas les mises à jour de l'objet précédemment observé.

Si vous faites cela dans la classe de l'objet d'observation, rappelez-vous simplement les objets que vous observez (ou, si vous n'observez qu'un seul objet, si vous l'observez). Cela suppose que l'observation est dynamique et entre deux objets autrement sans rapport; si l'observateur est propriétaire de l'observé, ajoutez simplement l'observateur après avoir créé ou conservé l'observateur, et supprimez l'observateur avant de relâcher l'observé.

L'ajout et la suppression d'un objet en tant qu'observateur devraient généralement se produire dans la classe de l'observateur, et jamais dans celle de l'objet observé.

Peter Hosey
la source
14
Cas d'utilisation: vous souhaitez supprimer des observateurs dans viewDidUnload, ainsi que dans dealloc. Cela les supprime deux fois et lèvera l'exception si votre viewController est déchargé d'un avertissement de mémoire, puis également libéré. Comment proposez-vous de gérer ce scénario?
bandejapaisa
2
@bandejapaisa: À peu près ce que j'ai dit dans ma réponse: Gardez une trace de si j'observe et n'essayez d'arrêter d'observer que si je le suis.
Peter Hosey
41
Non, ce n'est pas une question intéressante. Vous ne devriez pas avoir à garder une trace de cela; vous devriez pouvoir simplement désenregistrer tous les écouteurs dans dealloc, sans vous soucier de savoir si vous avez atteint le chemin du code où il a été ajouté ou non. Cela devrait fonctionner comme removeObserver de NSNotificationCenter, qui ne se soucie pas de savoir si vous en avez réellement un ou non. Cette exception crée simplement des bogues là où aucun n'existerait autrement, ce qui est une mauvaise conception d'API.
Glenn Maynard
1
@GlennMaynard: Comme je l'ai dit dans la réponse, «Si vous coupez les notifications de l'observateur à son insu, attendez-vous à ce que les choses se cassent; plus précisément, attendez-vous à ce que l'état de l'observateur devienne périmé car il ne reçoit pas les mises à jour de l'objet précédemment observé. » Chaque observateur doit mettre fin à sa propre observation; le fait de ne pas le faire devrait idéalement être très visible.
Peter Hosey
3
Rien dans la question ne parle de supprimer les observateurs d' autres codes.
Glenn Maynard
25

FWIW, [someObject observationInfo]semble être nilsi someObjectn'a pas d'observateurs. Je ne ferais pas confiance à ce comportement, cependant, car je ne l'ai pas vu documenté. De plus, je ne sais pas lire observationInfopour avoir des observateurs spécifiques.

ma11hew28
la source
Savez-vous comment je peux récupérer un observateur spécifique? objectAtIndex:ne donne pas le résultat souhaité.)
Eimantas
1
@MattDiPasquale Savez-vous comment puis-je lire observationInfo dans le code? Dans les impressions, il sort bien, mais c'est un pointeur vers le vide. Comment dois-je le lire?
neeraj
observationInfo est une méthode de débogage documentée dans l'article de débogage de Xcode (quelque chose avec «magique» dans le titre). Vous pouvez essayer de le rechercher. Je peux dire que si vous avez besoin de savoir si quelqu'un observe votre objet, vous faites quelque chose de mal. Repensez votre architecture et votre logique.
Je l'ai
Source:NSKeyValueObserving.h
nefarianblack
plus 1 pour une impasse comique mais toujours une réponse quelque peu utile
Will Von Ullrich
4

La seule façon de procéder est de définir un indicateur lorsque vous ajoutez un observateur.

Leibowitzn
la source
3
Si vous vous retrouvez avec des BOOL partout, mieux vaut toujours créer un objet wrapper KVO qui gère l'ajout et la suppression de l'observateur. Cela peut garantir que votre observateur n'est supprimé qu'une seule fois. Nous avons utilisé un objet comme celui-ci, et cela fonctionne.
bandejapaisa
excellente idée si vous n'observez pas toujours.
Andre Simon
4

Lorsque vous ajoutez un observateur à un objet, vous pouvez l'ajouter à un NSMutableArraycomme ceci:

- (void)addObservedObject:(id)object {
    if (![_observedObjects containsObject:object]) {
        [_observedObjects addObject:object];
    }
}

Si vous souhaitez annuler l'observation des objets, vous pouvez faire quelque chose comme:

for (id object in _observedObjects) {
    if ([object isKindOfClass:[MyClass class]]) {
        MyClass *myObject = (MyClass *)object;
        [self unobserveMethod:myObject];
    }
}
[_observedObjects removeAllObjects];

N'oubliez pas que si vous désobservez un seul objet, supprimez-le du _observedObjectstableau:

- (void)removeObservedObject:(id)object {
    if ([_observedObjects containsObject:object]) {
        [_observedObjects removeObject:object];
    }
}
Oritm
la source
1
Si cela se produit dans un monde multi-threadé, vous devez vous assurer que votre tableau est ThreadSafe
shrutim
Vous conservez une référence forte d'un objet, ce qui augmenterait le nombre de retenues à chaque fois qu'un objet est ajouté dans la liste et ne sera pas désalloué à moins que sa référence ne soit supprimée du tableau. Je préférerais utiliser NSHashTable/ NSMapTablepour conserver les références faibles.
atulkhatri
3

À mon avis - cela fonctionne de manière similaire au mécanisme de retentionCount. Vous ne pouvez pas être sûr qu'en ce moment vous avez votre observateur. Même si vous vérifiez: self.observationInfo - vous ne pouvez pas savoir avec certitude que vous aurez / n'aurez pas d'observateurs à l'avenir.

Comme keepCount . Peut-être que la méthode observationInfo n'est pas vraiment inutile, mais je ne l'utilise qu'à des fins de débogage.

Donc, en conséquence - il vous suffit de le faire comme dans la gestion de la mémoire. Si vous avez ajouté un observateur, supprimez-le simplement lorsque vous n'en avez pas besoin. Comme utiliser les méthodes viewWillAppear / viewWillDisappear etc. Par exemple:

-(void) viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self addObserver:nil forKeyPath:@"" options:NSKeyValueObservingOptionNew context:nil];
}

-(void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [self removeObserver:nil forKeyPath:@""];
}

Et si vous avez besoin de vérifications spécifiques - implémentez votre propre classe qui gère un tableau d'observateurs et utilisez-la pour vos vérifications.

quarezz
la source
[self removeObserver:nil forKeyPath:@""]; doit y aller avant: [super viewWillDisappear:animated];
Joshua Hart
@JoshuaHart pourquoi?
quarezz
Parce que c'est une méthode de démontage (dealloc). Lorsque vous remplacez une sorte de méthode de démontage, vous appelez super last. Comme: - (void) setupSomething { [super setupSomething]; … } - (void) tearDownSomething { … [super tearDownSomething]; }
Joshua Hart le
viewWillDisapear n'est pas une méthode de démontage et n'a aucun lien avec dealloc. Si vous avancez dans la pile de navigation, viewWillDisapear sera appelé, mais votre vue restera en mémoire. Je vois où vous allez avec la logique de configuration / démontage, mais le faire ici ne donnera aucun avantage réel. Vous ne voudrez placer la suppression avant super que si vous avez une logique dans la classe de base, qui pourrait entrer en conflit avec l'observateur actuel.
quarezz le
3

[someObject observationInfo]retourne nils'il n'y a pas d'observateur.

if ([tableMessage observationInfo] == nil)
{
   NSLog(@"add your observer");
}
else
{
  NSLog(@"remove your observer");

}
Anupama
la source
Conformément à la documentation Apple: observationInfo renvoie un pointeur qui identifie des informations sur tous les observateurs enregistrés auprès du récepteur.
FredericK
C'était mieux dit dans la réponse de @ mattdipasquale
Ben Leggiero
2

L'intérêt du modèle d'observateur est de permettre à une classe observée d'être "scellée" - de ne pas savoir ou de se soucier de savoir si elle est observée. Vous essayez explicitement de briser ce modèle.

Pourquoi?

Le problème que vous rencontrez est que vous supposez que vous êtes observé alors que vous ne l'êtes pas. Cet objet n'a pas démarré l'observation. Si vous souhaitez que votre classe contrôle ce processus, vous devez envisager d'utiliser le centre de notification. De cette façon, votre classe a un contrôle total sur le moment où les données peuvent être observées. Par conséquent, peu importe qui regarde.

adonoho
la source
10
Il demande comment l' auditeur peut savoir s'il écoute quelque chose, pas comment l'objet observé peut savoir s'il est observé.
Glenn Maynard
1

Je ne suis pas fan de cette solution try catch, donc ce que je fais la plupart du temps, c'est que je crée une méthode d'abonnement et de désabonnement pour une notification spécifique à l'intérieur de cette classe. Par exemple, ces deux méthodes abonnent ou désabonnent l'objet à la notification clavier globale:

@interface ObjectA : NSObject
-(void)subscribeToKeyboardNotifications;
-(void)unsubscribeToKeyboardNotifications;
@end

À l'intérieur de ces méthodes, j'utilise une propriété privée qui est définie sur true ou false en fonction de l'état de l'abonnement, comme suit:

@interface ObjectA()
@property (nonatomic,assign) BOOL subscribedToKeyboardNotification
@end

@implementation

-(void)subscribeToKeyboardNotifications {
    if (!self.subscribedToKeyboardNotification) {
        [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(onKeyboardShow:) name:UIKeyboardWillShowNotification object:nil];
        [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(onKeyboardHide:) name:UIKeyboardWillHideNotification object:nil];
        self.subscribedToKeyboardNotification = YES;
    }
}

-(void)unsubscribeToKeyboardNotifications {
    if (self.subscribedToKeyboardNotification) {
        [[NSNotificationCenter defaultCenter]removeObserver:self name:UIKeyboardWillShowNotification object:nil];
        [[NSNotificationCenter defaultCenter]removeObserver:self name:UIKeyboardWillHideNotification object:nil];
        self.subscribedToKeyboardNotification = NO;
    }
}
@end
Sebastian Boldt
la source
0

En plus de la réponse d'Adam, je voudrais suggérer d'utiliser une macro comme celle-ci

#define SafeRemoveObserver(sender, observer, keyPath) \
@try{\
   [sender removeObserver:observer forKeyPath:keyPath];\
}@catch(id anException){\
}

exemple d'utilisation

- (void)dealloc {
    SafeRemoveObserver(someObject, self, somePath);
}
Wattson
la source
1
À quel point est-ce fou qu'il jette une exception? Pourquoi ne fait-il rien si rien n'est attaché?
Aran Mulholland