NSMenuItem custom view not updating

597 views Asked by At

I have a NSStatusItem object which is created when the app launches (in AppDelegate.m). This object "_statusBarItem" has a Menu attached to it, of class statusBarMenu, which is subclass of NSMenu class but it also has a _panelItem property (of class NSMenuItem) created when you create an instance of statusBarMenu, as you can see below.

In statusBarItem.m

- (instancetype)initWithTitle:(NSString *)aTitle{
self = [super initWithTitle:aTitle];

if (self){

    _panelItem = [[NSMenuItem alloc]init];
    PanelViewController *panelViewController = [[PanelViewController alloc]init];
    panelViewController.menuDelegate = self;
    _panelItem.view = panelViewController.view;

    [self addItem:_panelItem];
}
return self;
}

The _panelItem has a custom view i.e. a clock in a label (among other things). This view is controlled by PanelViewController class, in which when viewDidLoad method is called calls the tickTock: method shown below. _upTime is the label showing, the clock/time. It is created in the .xib file and connected

- (void)tickTock:(id)obj{
NSTimeInterval timeInterval = 7.5 * 60 * 60;

NSDate *timeToGetUp = [NSDate dateWithTimeInterval:timeInterval sinceDate:[NSDate date]];

NSDateFormatter *dateFormatter = [NSDateFormatter new];
[dateFormatter setDateFormat:@"hh:mm:ss"];

[_upTime setStringValue:[dateFormatter stringFromDate:timeToGetUp]];

[self.view setNeedsDisplay:YES];
[self.menuDelegate refreshView:self];

[self performSelector:@selector(tickTock:) withObject:NULL afterDelay:1.0];
}

As you can see that tickTock: method is called every 1.0 second. This is because I want the label to update, every second with new time. However, the label does not update even though I call setNeedsDisplay: for the PanelViewController's view. I thought this might be because I might be updating the wrong view i.e. I should have been updating the _panelItem's view, instead. So I made a menuDelegate property and made statusBarMenu conform to the protocol show below.

@protocol PanelViewControllerMenuDelgate

- (void)refreshView:(id)obj;

@end

Again the refreshView: method is called every second, it updates the panel view.

- (void)refreshView:(id)obj{
[_panelItem.view setNeedsDisplay:YES];
//    [self itemChanged:_panelItem];
}

However, that still does not refresh the view, and show the new label value. I also tried itemChanged: method to the statusBarMenu obj (_statusBarItem) itself, although it did not have any different results.

What I did notice is that if I close the menu (by clicking somewhere else) and re-open it, it does show the new value of the clock/label. So what am I doing wrong, or what am I not doing, that is making the view stay the same. Should I be using some other procedure to make the _panelItem's view refresh every second?

EDIT:

I also noticed that the simulation stop running, when every click the _statusBarItem. I simply added a NSLog statement in tickTock: method and it stopped printing in the console, when ever I open the menu. This is probably why the view is not updating, since the app/Xcode pauses when ever I click the menu. Why does that happen? Also how can I stop it?

1

There are 1 answers

0
lindon fox On BEST ANSWER

I just came up against this problem a few weeks ago. In my case, my NSTimer was not updating my NSMenuItem. I believe your problem is happening because the menu is updating on a different run loop to the perform with delay. This question is answered in a few places. But this is the code you need:

NSTimer interfaceUpdateTimer = [NSTimer timerWithTimeInterval:self.timerSettings.timerUpdateInterval//one second in your case
                                         target:self
                                                  selector:@selector(tickTock:)
                                       userInfo:nil
                                        repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:interfaceUpdateTimer forMode:NSRunLoopCommonModes];

The key is NSRunLoopCommonModes. Check out the other answers I linked to; they already explain it really well. That should do the trick I think.


As a side note, neither the NSTimer nor the performSelector:withObject:afterDelay: are guaranteed to fire at exactly the time you specify. So if you need accuracy, do not rely on them to tell the time. If close enough is good enough, then you can ignore this note : )