I created a Mac app in Xcode. The only code changes I made were in the app delegate. In its entirety, it now reads like this:
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
}
- (void) readyToTerminate: (id) sender {
NSLog(@"Ready to terminate");
[NSApplication.sharedApplication replyToApplicationShouldTerminate:YES];
}
- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender {
[self performSelector:@selector(readyToTerminate:) withObject:self afterDelay:1.0];
NSLog(@"Later please");
return NSTerminateLater;
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
NSLog(@"Terminating");
}
@end
When I run the app, I do see a window. I then hit command-Q to terminate. The output is:
2018-04-01 17:20:03.110563-0500 TerminateThisApp[3336:364401] Unknown Window
class (null) in Interface Builder file,
creating generic Window instead
2018-04-01 17:20:06.929175-0500 TerminateThisApp[3336:364401] Later please
So readyToTerminate: is not getting called. Why would that be? I don't understand.
For what its worth, I tried an NSTimer with the same non-result. I also tried calling readyToTerminate the same way from didFinishLaunching. In that case, readyToTerminate did fire (and the app didn't terminate, which is fine).
What is going on?
I just built a quick macOS app on Xcode 9.3 with your code, and noticed that a launch services exception is thrown by the selector (increase your time out to see it clearly when you step through the code in debugger).
If you look in the Console
app and filter by your app name (e.g. I used SO-49603218
) do you see an entry like this:
default 09:44:00.031347 +1000 SO-49603218 LSExceptions shared instance invalidated for timeout.
default 09:44:55.906555 +1000 SO-49603218 Later please
I think it's possible that the app reference the timer is referring to i.e. self
is being invalidated. This may have something to do with run loop being switched to NSModalPanelRunLoopMode
when you reply with NSTerminateLater
If you change to NSTerminateCancel
then your readyToTerminate:
method will be called.
default 09:55:28.851246 +1000 SO-49603218 Later please
default 09:55:28.864566 +1000 SO-49603218 LSExceptions shared instance invalidated for timeout.
default 09:55:29.851891 +1000 SO-49603218 Ready to terminate
Note that the LSException
is still thrown 🤔
I think after reading more on the Anatomy of a Run Loop documentation the problem is the performSelector:
call is made on the NSDefaultRunLoopMode
so it won't be called until you return to that run mode. From the documentation, under Timer Sources:
Like input sources, timers are associated with specific modes of your run loop. If a timer is not in the mode currently being monitored by the run loop, it does not fire until you run the run loop in one of the timer’s supported modes.