12

I am communicating between two classes with NSNotificationCenter. My problem is that although I tap a button once (and that button only fires off once) I am unintentionally producing increasing numbers of notifications from only one call to the NSNotificationCenter.

Here is a better explanation of the problem, with code:


My two classes are the mainView class and the Menu class.

When a view in the mainView class is tapped, it launches a view created and governed by the Menu class. This code is called when the mainView is initialized:

menu=[[MyMenu alloc] init];
UITapGestureRecognizer * tap=[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapped:)];
[tap setNumberOfTapsRequired:1];
[container addGestureRecognizer:tap];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onChangeItem:) name:@"ItemChange" object:nil];

This Gesture Recognizer fires off this method, also in the mainView class:

- (void) onTapped: (UIGestureRecognizer*) recognizer {
    NSLog(@"tap");
    [menu displayMenu];
}

This is how the Menu class initializes:

- (MyMenu*) init {
    self=[super init];
    UICollectionViewFlowLayout * layout=[[UICollectionViewFlowLayout alloc] init];
    menuView=[[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 200, 200) collectionViewLayout:layout];
    [menuView setDataSource:self];
    [menuView setDelegate:self];
    [menuView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cell"];
    [menuView setAutoresizesSubviews:YES];
    [menuView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
    [menuView setBackgroundColor:[UIColor clearColor]];
    [menuView setIndicatorStyle:UIScrollViewIndicatorStyleWhite];
    return self;
}

And this is the displayMenu method inside the Menu class:

- (void) displayMenu {
    [viewForMenu addSubview:menuView];
}

The Menu class also has a clearMenu method:

- (void) clearMenu {
    [menuView removeFromSuperview];
}

This is the code for each cell in the UICollectionView, contained within my Menu class:

- (UICollectionViewCell*) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell * cell=[collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
    [cell setTag:indexPath.row];
    UITapGestureRecognizer * tap=[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onButtonTapped:)];
    [tap setNumberOfTapsRequired:1];
    [cell addGestureRecognizer:tap];
    NSLog(@"button tapped : %d",indexPath.row);
    return cell;
}

This calls the onButtonTapped: method, also within my Menu class:

- (void) onButtonTapped:(UIGestureRecognizer*) recognizer {
    NSInteger buttonTapped=[[recognizer view] tag];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"ItemChange" object:nil userInfo:@{@"selected":@(buttonTapped)}];
[self clearMenu];
}

This notification is picked up by my mainView class with this code:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onChangeItem:) name:@"ItemChange" object:nil];

This calls the onChangeItem: method, inside my mainView class:

- (void) onChangeItem: (NSNotification*) notification {
    NSLog(@"change item to %d",[[[notification userInfo] objectForKey:@"clock"] intValue]);
}

So that's the code.


OK, here's the problem: the first time the menu displays I get this in my log:

...[43023:11f03] tap
...[43023:11f03] button tapped : 1
...[43023:11f03] change item to 1

And this is fine, this is what I expect. However second time around I get this:

...[43023:11f03] tap
...[43023:11f03] button tapped : 1
...[43023:11f03] change item to 1
...[43023:11f03] change item to 1

Third time around I get this:

...[43023:11f03] tap
...[43023:11f03] button tapped : 1
...[43023:11f03] change item to 1
...[43023:11f03] change item to 1
...[43023:11f03] change item to 1
...[43023:11f03] change item to 1

And so on. Each successive tap on a menu item doubles the amount of notification calls.


To begin with I thought I was adding multiple views, and thus resulting in multiple button taps, and therefore multiple notifications calls.

However as you can see from my logs, this is not the case. The buttons are only receiving 1 tap event - this is firing off only 1 notification - but receiving class gets sent multiple notifications.

Can anyone explain this to me?

Sorry for the lengthy post!

Jimmery
  • 9,783
  • 25
  • 83
  • 157
  • Where in the code you are calling `[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onChangeItem:) name:@"ItemChange" object:nil];` and removing it ? In your `onChangeItem:` method print memory address of `self` which is observing the notification `NSLog(@"%p",self);` and show us what you see in console. – 0x8badf00d Aug 07 '13 at 16:06
  • This is called when the **mainView** is initialized. So it is called only once. – Jimmery Aug 07 '13 at 16:06
  • Can you add `NSLog(@"%p",self);` in your `onChangeItem:` method and show us what you see in console – 0x8badf00d Aug 07 '13 at 16:09
  • 1
    You're adding a new tap gesture recognizer to the cell every time a cell is reused. You should check if one exists on the cell first before adding one. – rdelmar Aug 07 '13 at 18:17
  • @rdelmar How would I check if one exists? – Jimmery Aug 08 '13 at 08:02

2 Answers2

36

Well, I am assuming that [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onChangeItem:) name:@"ItemChange" object:nil]; are being added more than once.

I like to remove any potential observer before adding an observer, like so:

[[NSNotificationCenter defaultCenter] removeObserver:self name:@"ItemChange" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onChangeItem:) name:@"ItemChange" object:nil];

That way there will ever only be one observer callback.

CodeReaper
  • 5,988
  • 3
  • 35
  • 56
  • @Jimmery If you do a global search for "addObserver:" is there a "removeObserver:" above each one? – CodeReaper Aug 08 '13 at 08:44
  • Oh. Ok. I am an idiot, I put your code in the wrong place. Now that it is in the right place it fixes my bug! Thank you so much :) I wish I could offer you more than just fake internet points! – Jimmery Aug 08 '13 at 09:05
  • 3
    i have the same problem, but iam sure that i have exactly called addObserver just once, but iam getting the callback twice, with exectly the same notification object (notifications have the same memory adress and content) – Peter Lapisu Jan 27 '15 at 12:50
  • 1
    4 years later and you get more fake internet points! This helped me out of a jam! – PruitIgoe Jul 25 '17 at 14:39
1

Issue: I faced same problem, observer was calling twice, sometimes thrice.

Scenario

  1. A user taps logout button
  2. HomeViewController was dismissed and LoginViewController screen was presented
  3. When user signed in again for second time
  4. Observer was called twice (sometimes thrice)

The issue was [[NSNotificationCenter defaultCenter] removeObserver:self]; in dealloc method of my HomeViewController was not called at all, which actually removes an object from the notification center all together.

ℹ️ dealloc is an Objective-C selector that is sent by the Objective-C runtime to an object when the object is no longer owned by any part of the application.

Solution: Made your own method dispose and call it when user tap logout.

- (void)dealloc { 
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    // ....
}

- (void)dispose { 
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)logoutTapped { 
    [self dispose];
    // ....
}
Baig
  • 4,737
  • 1
  • 32
  • 47