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!
Since you mentioned being new to macOS/Obj-C, let's start off with some background information:
.app
(likeFirefox.app
). Those are actually just directories following a specific format. Directly inside is a dirContents
, which at bare minimum contains anInfo.plist
file andMacOS
dir (there might also always be aPkgInfo
file, not entirely sure). And finally, inside theMacOS
dir is the actual compiled executable.Contents/Frameworks
andContents/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 containsData/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 executesFirefox.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 anInfo.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 runopen -a Firefox
and it will look for an app bundle named exactlyFirefox.app
. You can doopen -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 runopen /some/path/some.app
to open that specific application.open
pass arguments along. Again with Firefox as an example, you can doopen -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 likeopen someapp.app
did work just fine though. I compared the Console output when running it both ways, and when it crashes it showsno 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 optionMain Interface
. Clear that box.Info.plist
:Application is agent (UIElement)
with valueYES
. The actual XML key isLSUIElement
.AppDelegate
.AppDelegate
class'sapplicationDidFinishLaunching
function.main.m
should look something like this: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 theopen
command block until the application quits, but interrupting it wouldn't actually send the signal to your application (but rather still toopen
). Hence thekillall
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 itloginwindow
or something. =] Thetrap
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.