我正在尝试从独立的Objective-C文件中抛出通知。NSUserNotification
API 将在 OSX 11 之后弃用,因此我希望切换到较新的UNUserNotification
接口。
不幸的是,我无法从谷歌搜索中找到太多关于这个话题的信息。我有以下代码抛出错误:
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
在同一目录中:
<?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>
这是使用cc -framework Cocoa -framework UserNotifications -o app notif.m
编译的。Info.plist
会自动合并,因此应该不会出现捆绑问题。
不幸的是,运行./app
后出现以下错误:
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'
我是MacOS/Objective-C开发的新手,无法解析此消息。我也无法理解我在谷歌上能找到的东西。任何见解将不胜感激;非常感谢!
既然你提到是macOS/Obj-C的新手,让我们从一些背景信息开始:
- macOS 有不同的应用程序形式。就像您正在编写的那样,有一些"命令行"程序(即无GUI)通常采用单个独立二进制文件的形式。
- 还有"应用程序包",它们是以
.app
结尾的应用程序(如Firefox.app
)。这些实际上只是遵循特定格式的目录。直接里面是一个目录Contents
,它至少包含一个Info.plist
文件和MacOS
目录(也可能总是有一个PkgInfo
文件,不完全确定)。最后,MacOS
目录内部是实际编译的可执行文件。 - (还有更多形式,但让我们保持这两个)
- 自定义/第三方框架和库或任何杂项资源之类的东西可以嵌入到应用程序包中,这就是为什么您可以在 Mac 上复制它们并且它可以正常工作(减去任何首选项)。通常这些分别
Contents/Frameworks
和Contents/Resources
,但我相信你可以使用任何你想要的名字。 - 首选项通常存储在
$HOME/Library/Preferences
但是如果应用程序是沙盒的,则它在$HOME/Library/Containers
中有一个容器,然后包含Data/Library/Preferences
。 Info.plist
(属性列表)文件包含有关应用程序的一些信息,例如"捆绑标识符"。- 当您通过macOS的Finder运行Firefox时,它实际上会调用"启动服务"来实际启动浏览器。这将读取
.plist
文件并设置运行应用程序的环境,然后最终执行Firefox.app/Contents/MacOS/firefox
。 - 从终端运行诸如
./app
之类的东西不会通过LS(启动服务),而只是立即按原样执行代码。您确实提到在同一目录中有一个Info.plist
,但由于您绕过了 LS,因此它甚至没有被使用。 - 还有通过 LS 的
open
命令。您可以运行open -a Firefox
,它将查找名为Firefox.app
的应用程序包。您可以open -b org.mozilla.firefox
改用捆绑标识符,该标识符可能始终保持不变,因此对脚本更安全。您也可以简单地运行open /some/path/some.app
以打开该特定应用程序。 - 根据应用程序的设置方式,您可以传递参数
open
。再次以火狐为例,您可以执行open -a Firefox https://stackoverflow.com
。
现在,继续你的错误。在开发自己的应用程序时,我遇到了同样的问题,它实际上是一个应用程序包,但我通常也直接运行包含的二进制文件,因为它更容易。但是,在尝试获得currentNotificationCenter
时,这也总是会崩溃。像open someapp.app
一样运行它确实工作得很好。我比较了两种方式运行时的控制台输出,当它崩溃时,它会显示no registered bundle with URL <private>
.我猜通过绕过LS,"捆绑代理"将不会设置,并且事情开始崩溃。或者捆绑代理可能始终在运行,但我的应用程序的捆绑标识符根本没有在其"引擎"中注册(因为缺乏更好/已知的术语)。其他功能(例如创建对话框警报)在直接运行时仍然有效,因此看起来使用任何 GUI 功能都不会立即取消您在 LS 之外运行二进制文件的资格。我认为这是一种安全措施,可以防止任何基于 CLI 的程序发布通知。
有几种选择:
-
也许只是运行
open ./app
就可以了。至少在我的情况下,它会打开一个新的终端窗口,但崩溃消失了,通知确实有效。这可能是因为它仍然包含在应用程序包中,并且LS检测到了这一点。 -
否则,您可能不得不创建一个应用程序包。您仍然可以以这种方式创建无 GUI 的应用程序,但可能需要一些额外的步骤来删除 GUI,具体取决于您是否要使用 Xcode。我一直用它来创建一个基于
User Interface: XIB
的"应用程序"项目。项目创建向导不允许您选择任何接口,因此您需要删除该接口。你也许能够弄清楚如何在没有 Xcode 的情况下做到这一点,但我不知道 Xcode 在实际输出工作应用程序包时所做的 100%。因此,对于 Xcode:- 在项目导航器(应该是左侧边栏)中,单击顶级节点(您的项目名称)。
- 在左内侧边栏中选择"目标"而不是项目本身。
- 然后在顶部标签栏上,确保选中
General
。 - 第一部分之一应
Deployment Info
,并带有选项Main Interface
。清除该框。 - 从项目中删除实际的 XIB 文件。
- 向
Info.plist
添加新行:Application is agent (UIElement)
,值为YES
。实际的 XML 密钥是LSUIElement
。 - 检查用作应用程序主"委托"的 Cocoa 类的名称。默认情况下,这应该只是
AppDelegate
. - 将原始代码移动到
AppDelegate
类的applicationDidFinishLaunching
函数中。 main.m
中的入口点应如下所示:
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 =]
}
现在,当您运行应用程序时,Dock 上没有图标,因为它已设置为"代理"。它甚至不会尝试呈现任何视图,因为我们删除了这些视图。当然,如果您想接受来自终端的用户输入,这将不太有效。可能有一些东西,但我相信这超出了这个问题的范围。
只剩下一个细节:通过LS从终端运行应用程序,并能够按Ctrl + C/中断它。实现类似行为的唯一方法可能是trap : INT; open -W someapp.app; killall someapp 2>/dev/null
。-W
标志使open
命令阻止,直到应用程序退出,但中断它实际上不会将信号发送到您的应用程序(但仍会发送到open
)。因此,killall
杀死任何以前的实例,如果应用程序已经自行退出/异常,则重定向到 null 以抑制任何错误。只是必须确保你不要称它为loginwindow
或其他什么。=]trap
用于确保中断信号仅适用于当前正在运行的命令,而不是一次应用于整个链。您甚至可以将该行包装在一个好的旧.sh
脚本中,陷阱也会使它也不会中止整个脚本。