Updating an NSmenu from an asynchronous NSURLConne

2019-06-25 11:50发布

问题:

I am writing a little systray application that fetches data from an API and updates its menu accordingly, and am having trouble with updating the menu while it's open.

I don't even know where to start, so let's start with the beginning.

I have a custom PNLinksLoader class whose responsibility is to fetch the data and parse it:

- (void)loadLinks:(id)sender
{
    // instance variable used by the NSXMLParserDelegate implementation to store data
    links = [NSMutableArray array];

    [NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
        NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
        parser.delegate = self;

        if (YES == [parser parse]) {
            NSArray *modes = [NSArray arrayWithObject:NSRunLoopCommonModes];
            [delegate performSelectorOnMainThread:@selector(didEndLoadLinks:) withObject:links waitUntilDone:NO modes:modes];
        }
    }];
}

The loader is ran one time at the application's startup (works perfectly), and then a timer is setup for periodic refresh:

loader = [[PNLinksLoader alloc] init];
[loader setRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://papyrus.pandanova.com/links"]]];

[loader setDelegate:self];
[loader loadLinks:self];

NSMethodSignature *signature = [loader methodSignatureForSelector:@selector(loadLinks:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:loader];
[invocation setSelector:@selector(loadLinks:)];

NSTimer *timer = [NSTimer timerWithTimeInterval:10 invocation:invocation repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Now whenever the loader loads new data on the server, if the menu is closed, then everything is ok, I open the menu and the new data is there.

But if the menu happens to be open during the refresh, then nothing happens. If I close and reopen the menu, then I can see the new data.

I think I'm missing something about the RunLoop, but I fail to see what (my understanding of it is very sparse, as I'm actually writing this little app mainly to learn Objective-C).

EDIT

The problem here is not to update the menu while it is open, it actually kind of works when I use performSelector:withObject: instead of performSelectorOnMainThread:withObject:waitUntilDone:modes: in the loader. The thing is when I do that I get weird results when updating the menu while it's open (works perfectly when the menu is closed):

Adding an NSLog call in my menu population loop fixes the symptoms, and from what I read on the internet, it might be a sign that I have a race condition on my threads (that's why I tried using performSelectorOnMainThread, but I can't seem to figure it out.

回答1:

The problem is that when the menu is open, the current run loop mode is not contained in NSRunLoopCommonModes anymore: it becomes NSEventTrackingRunLoopMode.

So you have to add your timer to the current run loop for this mode too:

// use "scheduledTimer..." to have it already scheduled in NSRunLoopCommonModes, it will fire when the menu is closed
menuTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(fireMenuUpdate:) userInfo:nil repeats:YES];

// add the timer to the run loop in NSEventTrackingRunLoopMode to have it fired even when menu is open
[[NSRunLoop currentRunLoop] addTimer:menuTimer forMode:NSEventTrackingRunLoopMode];

Then in your timed method, when you receive the response to your request, call the method to update the menu on the main thread (the UI thread):

[NSURLConnection sendAsynchronousRequest:request
                                   queue:[[NSOperationQueue alloc] init]
                       completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {

                           [self performSelectorOnMainThread:@selector(updateMenu) withObject:nil waitUntilDone:NO modes:[NSArray arrayWithObject:NSRunLoopCommonModes]];

                       }];

And finally, in your update method, apply the changes to your menu data and don't forget to ask for the menu to update its layout:

[menu removeAllItems];

NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"HH:mm:ss"];
[menu addItemWithTitle:[formatter stringFromDate:[NSDate date]] action:nil keyEquivalent:@""];

[menu update];