Reputation: 137
I'm trying to throw notifications from a standalone Objective-C file. The NSUserNotification
API will be deprecated after OSX 11, so I'm looking to switch to the newer UNUserNotification
interface.
Unfortunately, I'm not able to find much on the topic from Googling. I have the following code that throws an error:
notif.m
:
#import <stdio.h>
#import <Cocoa/Cocoa.h>
#import <UserNotifications/UserNotifications.h>
#import <objc/runtime.h>
int native_show_notification(char *title, char *msg) {
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = [NSString stringWithUTF8String:title];
content.body = [NSString stringWithUTF8String:msg];
content.sound = [UNNotificationSound defaultSound];
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"NOTIFICATION" content:content trigger:trigger];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (!error) {
printf("NOTIFICATION SUCCESS ASDF");
}
}];
return 0;
}
int main() {
native_show_notification("Foo" , "Bar");
}
Info.plist
in the same directory:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.microsoft.VSCode</string>
</dict>
</plist>
This is compiled using cc -framework Cocoa -framework UserNotifications -o app notif.m
. The Info.plist
is incorporated automatically, so there shouldn't be a bundling issue.
Unfortunately, after running ./app
I get the following error:
Assertion failure in +[UNUserNotificationCenter currentNotificationCenter], UNUserNotificationCenter.m:54
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'bundleProxyForCurrentProcess is nil: mainBundle.bundleURL file:///path-to-folder-containing-the-source-files'
I'm new to MacOS/Objective-C development and am unable to parse this message. Couldn't understand things I could find on Google either. Any insights would be appreciated; thanks so much!
Upvotes: 4
Views: 1191
Reputation: 339
Since you mentioned being new to macOS/Obj-C, let's start off with some background information:
.app
(like Firefox.app
). Those are actually just directories following a specific format. Directly inside is a dir Contents
, which at bare minimum contains an Info.plist
file and MacOS
dir (there might also always be a PkgInfo
file, not entirely sure). And finally, inside the MacOS
dir is the actual compiled executable.Contents/Frameworks
and Contents/Resources
respectively but I believe you can use any names you want.$HOME/Library/Preferences
but if the application is sandboxed it has a container in $HOME/Library/Containers
, which then contains Data/Library/Preferences
.Info.plist
(property list) file contains some information about the application, such as the "bundle identifier"..plist
file and sets up the environment to run the application in, then finally executes Firefox.app/Contents/MacOS/firefox
../app
from a terminal does not pass through LS (Launch Services), but just immediately executes the code as-is. You did mention having an Info.plist
in the same directory but since you're bypassing LS it's not even being used.open
command which does pass through LS. You can run open -a Firefox
and it will look for an app bundle named exactly Firefox.app
. You can do open -b org.mozilla.firefox
to use the bundle identifier instead, which likely always remains the same and thus is safer for scripts. You can also simply run open /some/path/some.app
to open that specific application.open
pass arguments along. Again with Firefox as an example, you can do open -a Firefox https://stackoverflow.com
.Now, on to your error. I had the same issue when developing an application of my own, which was actually an app bundle but I also usually run the contained binary directly since it's just easier. However, this too would always crash when trying to get currentNotificationCenter
. Running it like open someapp.app
did work just fine though. I compared the Console output when running it both ways, and when it crashes it shows no registered bundle with URL <private>
. I'm guessing that by bypassing LS the "bundle proxy" won't be set up and things start crashing. Or the bundle proxy might always be running, but my application's bundle identifier simply wasn't registered with its "engine" (for lack of a better/known term). Other things such as creating dialog alerts do still work when running it directly, so it doesn't look like using just about any GUI feature immediately disqualifies you for running binaries outside of LS. I think it's a sort of security measure to prevent just any CLI-based program from posting notifications.
There are a couple of options:
Perhaps just running open ./app
will work. At least in my case it opens a new terminal window but the crash is gone and notifications do work. This might be due to it still being contained within an app bundle and LS detecting that, though.
Otherwise you're probably going to have to need to create an app bundle instead. You can still create a GUI-less application that way but it might require a few additional steps to remove the GUI, depending on if you want to use Xcode or not. I've always used it to just create an "App" project, based on User Interface: XIB
. The project creation wizard doesn't allow you to choose no interface so that's what you'll need to remove. You may be able to figure out how to do it without Xcode but I wouldn't know 100% of what Xcode does to actually output a working app bundle. So, for Xcode:
General
is selected.Deployment Info
with an option Main Interface
. Clear that box.Info.plist
: Application is agent (UIElement)
with value YES
. The actual XML key is LSUIElement
.AppDelegate
.AppDelegate
class's applicationDidFinishLaunching
function.main.m
should look something like this:int main(int argc, const char * argv[]) {
//@autoreleasepool {} // Probably not necessary, so can remove that
//return NSApplicationMain(argc, argv); // Remove this too
// Add all this, maybe change AppDelegate to match your class
AppDelegate *appDelegate = [[AppDelegate alloc] init];
NSApplication *application = [NSApplication sharedApplication];
[application setDelegate: appDelegate];
[application run]; // Is a blocking call, it never actually returns
return EXIT_SUCCESS; // Won't be reached but we need to return some int to suppress errors/warnings =]
}
Now when you run the application, there's no icon on the Dock since it has been set as an "agent". It also doesn't even try to render any views because we removed those. Of course this won't quite work if you want to accept user input from the terminal. There may be something for that but I believe that's out of scope for this question.
There's just one detail remaining: running the application from the terminal through LS and being able to Ctrl+C/interrupt it. Probably the only way to achieve similar behaviour would be trap : INT; open -W someapp.app; killall someapp 2>/dev/null
. The -W
flag makes the open
command block until the application quits, but interrupting it wouldn't actually send the signal to your application (but rather still to open
). Hence the killall
to kill any previous instances, redirected to null to suppress any errors if the application already quit on its own/abnormally. Just gotta make sure you don't call it loginwindow
or something. =] The trap
is there to ensure the interrupt signal only applies to the currently running command instead of the whole chain at once. You could even wrap that line in a good old .sh
script, the trap also makes it so it doesn't abort the entire script either.
Upvotes: 3