有时,我们需要在用户开机之后,自动启动某个应用。这在之前,实现起来是比较麻烦的,但是macOS 13简化了它。
我之前写过两篇文章,分别对应macOS 13之前的版本,和macOS 13。
但是其中macOS 13的实现方式存在一些问题。比如我经常习惯的方式是,用户登录后运行某个应用,同时这个应用启动后,自动将自身从Dock移除,然后隐藏到系统菜单栏。但是使用第二篇文章的方式,应用本身并没有正确隐藏,而是显示窗口,同时应用也没有自动从Dock中移除。
当时我的理解是,macOS 13就是这么设计的。但是随着应用的增多,每次登录都要点击多次关闭按钮,让我觉得挺麻烦的,于是打算深入研究这个问题。
之前的实现代码
private func setAutoStart() {
#if !DEBUG
let shouldEnable = Defaults[.autoLaunchWhenLogin]
if #available(macOS 13.0, *) {
if shouldEnable {
try? SMAppService().register()
} else {
try? SMAppService().unregister()
}
} else {
if !SMLoginItemSetEnabled("com.parussoft.Stand-Reminder-Launcher" as CFString, shouldEnable) {
fatalError()
}
}
#endif
}
对比新旧两段代码,我们发现新代码并没有指定"com.parussoft.Stand-Reminder-Launcher",经过阅读SMAppService
的文档,我发现,原来等效的代码应该是这个。
private func setAutoStart() {
#if !DEBUG
let shouldEnable = Defaults[.autoLaunchWhenLogin]
if #available(macOS 13.0, *) {
if shouldEnable {
try? SMAppService.loginItem(identifier: "com.parussoft.Stand-Reminder-Launcher").register()
} else {
try? SMAppService.loginItem(identifier: "com.parussoft.Stand-Reminder-Launcher").unregister()
}
} else {
if !SMLoginItemSetEnabled("com.parussoft.Stand-Reminder-Launcher" as CFString, shouldEnable) {
fatalError()
}
}
#endif
}
检验结果
是不是使用等效的代码就可以了呢?让我们验证一下。经过多次重启,以及注销-重新登录的操作,我发现等效的新代码,在macOS 13中并不是次次都能生效。有的时候,会出现主程序并没有被成功呼唤出来的现象。
当时我怀疑是不是并不是每次辅助程序都成功运行了。可是经过我的进一步测试,实际上辅助程序每次都是成功运行的,但是辅助程序调用主程序的时候,并不是每次都能成功调用。
所以我们需要修改的是调用的代码。
旧版
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
UserDefaults.shared.set(true, forKey: UserDefaults.Key.startFromLauncher.rawValue)
let pathComponents = Bundle.main.bundleURL.pathComponents
let mainRange = 0..<(pathComponents.count - 4)
let mainPath = pathComponents[mainRange].joined(separator: "/")
try! NSWorkspace.shared.launchApplication(at: URL(fileURLWithPath: mainPath, isDirectory: false), options: [], configuration: [:])
NSApp.terminate(nil)
}
}
改进代码如下,当调用主程序遇到问题时,等待两秒再重试,直到成功调用后,退出辅助程序。
新版
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
UserDefaults.shared.set(true, forKey: UserDefaults.Key.startFromLauncher.rawValue)
let pathComponents = Bundle.main.bundleURL.pathComponents
let mainRange = 0..<(pathComponents.count - 4)
let mainPath = pathComponents[mainRange].joined(separator: "/")
let url = URL(fileURLWithPath: mainPath, isDirectory: false)
let configuration = NSWorkspace.OpenConfiguration()
launchMainApp(url, with: configuration)
}
private func launchMainApp(_ url:URL, with configuration:NSWorkspace.OpenConfiguration) {
NSWorkspace.shared.openApplication(at: url, configuration: configuration) { app, error in
if error == nil {
NSApp.terminate(nil)
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { [unowned self] in
launchMainApp(url, with: configuration)
}
}
}
}
}
再次测试,发现一切正常,问题解决。
更简单的实现方式
传统的实现方式步骤是这样的:
- 创建主程序
- 创建辅助程序
- 在主程序中注册辅助程序为启动项
- 经过一些编译设置,达到如下的效果
- 用户登录之后,辅助程序先启动
- 辅助程序启动后调用主程序
- 辅助程序退出
新版也支持上面的方式。但是新版同时支持了更简单的实现方式。
private func setAutoStart() {
#if !DEBUG
let shouldEnable = Defaults[.autoLaunchWhenLogin]
do {
if shouldEnable {
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
} catch {
print(error)
}
#endif
}
只需要将loginItem
改为mainApp
,就能够实现主应用在用户登录后自动启动。不过这种方式相比于原来的方式,比较粗糙,主程序只能有一种状态,只能隐藏或者不隐藏,而不能根据启动的方式不同来执行不同的任务。
这种方式的好处是简单,只需要有这段代码就可以了。不需要做额外的设置。
理解macOS 13的登录项设置
macOS 13刚出的时候,大家都对于登录项这个设置很陌生,不理解为什么它会分成两栏。根据我这段时间的测试,我目前已经理解了它两栏的含义。
如图是我目前系统中的登录项的设置。它有两项,一个叫登录时打开,一个叫允许在后台。很多人不理解它们的区别,下面我来解释一下。
登录时打开
登录时打开,只的是登录时,直接打开主应用。也就是说,应用的代码中应该要包含类似如下的代码:
try SMAppService.mainApp.register()
如果你的应用中不含这样的代码,就算你手动添加应用,添加完成之后,被添加的应用也不会在列表中出现。
允许在后台
允许在后台则是另外的情况,你的应用中需要包含类似如下的代码:
try? SMAppService.loginItem(identifier: "com.parussoft.Stand-Reminder-Launcher").register()
即你的应用中包含了辅助应用。辅助应用被系统在后台唤醒,然后辅助应用调用主应用。根据范围的不同,辅助应用可以分为Login Item,Login Agent以及Login Daemons。
What are the differences between LaunchAgents and LaunchDaemons?
调试时需要的注意事项
在开发者的电脑里,可能同时存在应用的多个版本,比如开发版,正式版等,它们存在于电脑中的多个位置。这可能导致在用户登录后,你看到的现象并非来至于你目前在调试的版本。
解决办法是用聚焦来进行搜索,删除掉应用的所有版本,然后在Xcode中重新运行当前版本,这样就能保证系统目前只有一个版本,方便调试。
允许在后台的删除
允许在后台包含了三种情况,其中Login Item对应的是应用程序本身,删掉应用就会自动删除。另外两种情况则需要找到对应的文件夹,删掉自动添加的配置文件才可以。