MacOS开发中很多场景会用到登录后应用自动运行功能,网上能查阅到的有限几篇文章大多都是object-c实现,并且版本比较久远,大多是关于macOS 11前后版本的实现,需要修改调整的地方较多 阅读本文,您将实现以swift来编码,同时兼容原有旧系统api以及macOS 13 Ventura新特性的自启动应用。
- Swift实现登录自启动
- 兼容macOS 13 Ventura,新Api SMAppService
- 自启动应用上线审核规范
新建SwiftUI工程TestAutoStartApp
import SwiftUI
@main
struct TestAutoStartAppApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@MainActor
private final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
//在启动主项目的时候 发送通知 关闭开机启动帮助程序
DistributedNotificationCenter.default.post(name: NSNotification.Name(rawValue: "TerminateAppHelperNotificationName"), object: Bundle.main.bundleIdentifier)
}
}
主界面
import SwiftUI
struct ContentView: View {
/** 开机启动 */
@State private var startup = false
var body: some View {
VStack(alignment: .leading) {
HStack {
HStack {
Text(LocalizedStringKey("开机启动"))
Text(":")
}
.frame(width: 80.0, alignment: .trailing)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20))
Toggle("", isOn: $startup)
.controlSize(.large)
.onChange(of: startup) { newValue in
// if !canStartupChange {
// return
// }
// canStartupChange = true
//print(newValue)
if Preference.startup == newValue {
return
}
Preference.startup = newValue
AppAutoStartUtils.setAppAutoStart(isAutoStart: newValue)
}
//.toggleStyle(CheckBoxToggleStyle(shape: .square))
}
//.frame(width: 200.0, height: 100.0)
}
.frame(width: 240, height: 90)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
Preference类:存储自启动开启/关闭状态
import Foundation
extension UserDefaults {
// 通过下标使用枚举
subscript<T: RawRepresentable>(key: String) -> T? {
get {
if let rawValue = value(forKey: key) as? T.RawValue {
return T(rawValue: rawValue)
}
return nil
}
set { set(newValue?.rawValue, forKey: key) }
}
subscript<T>(key: String) -> T? {
get { return value(forKey: key) as? T }
set { set(newValue, forKey: key) }
}
}
struct Preference {
/** 开机启动 */
static var startup: Bool {
get { return UserDefaults.standard[#function] ?? false }
set { UserDefaults.standard[#function] = newValue }
}
}
AppAutoStartUtils类:设置自启动工具类
//
// AppAutoStartUtils.swift
// TestAutoStartApp
//
// Created by bin zhu on 2023/7/2.
//
import Foundation
import ServiceManagement
import AppKit
class AppAutoStartUtils {
static func setAppAutoStart(isAutoStart: Bool) {
//这里注意改成自己的帮助工程名称
var launcherAppUrl = Bundle.main.bundleURL
launcherAppUrl = launcherAppUrl.appendingPathComponent("Contents/Library/LoginItems/LaunchHelper.app")
let launcherAppPath = launcherAppUrl.path
//print(launcherAppPath)
if !FileManager.default.fileExists(atPath: launcherAppPath) {
print("launcherApp no exist")
print("(\(launcherAppPath))")
return
}
if #available(macOS 13.0, *) {
if isAutoStart {
addLoginItem()
}
else {
removeLoginItem()
}
} else {
if (!SMLoginItemSetEnabled(AppConfig.LauncherAppBundleId as CFString, isAutoStart))
{
print("SMLoginItemSetEnabled failed!");
}
}
var alreadRunning = false;
let runnings = NSWorkspace.shared.runningApplications;
for app in runnings {
//这里是帮助项目的bundle identifier
if app.bundleIdentifier == AppConfig.LauncherAppBundleId {
alreadRunning = true;
break;
}
}
if alreadRunning {
//发送终止帮助程序的通知
DistributedNotificationCenter.default().post(name: NSNotification.Name(rawValue: "TerminateAppHelperNotificationName"), object: Bundle.main.bundleIdentifier)
}
}
// 创建登录项
@available(macOS 13.0, *)
private static func loginItem() -> SMAppService {
// The bundle identifier of the helper application bundle.
return SMAppService.loginItem(identifier: AppConfig.LauncherAppBundleId)
}
// 添加登录项
@available(macOS 13.0, *)
static func addLoginItem() {
do {
try loginItem().register()
print("auto start register ok")
} catch {
print(error)
}
}
// 移除登录项
@available(macOS 13.0, *)
static func removeLoginItem() {
do {
try loginItem().unregister()
print("auto start unregister ok")
} catch {
print(error)
}
}
}
运行效果
新建Target
名称LaunchHelper,使用xib就可以,这个Target是后台运行没有显示界面
import Cocoa
@main
class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
//主项目的bundle identifier
let mainAppBundleId = "com.cosin.TidyBar"
var alreadRunning = false
let runnings = NSWorkspace.shared.runningApplications
for app in runnings {
if app.bundleIdentifier == mainAppBundleId {
alreadRunning = true
break;
}
}
if (!alreadRunning) {
//这里是注册的关闭帮助程序的通知监听
DistributedNotificationCenter.default().addObserver(self, selector: #selector(terminate), name: NSNotification.Name(rawValue: "TerminateAppHelperNotificationName"), object: mainAppBundleId)
guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: mainAppBundleId) else {
print("bundleId not found(\(mainAppBundleId)")
return
}
let configuration = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.openApplication(at: url, configuration: configuration, completionHandler: nil)
}
else {
NSApp.terminate(nil)
}
}
@objc fileprivate func terminate(){
//方法调用
NSApp.terminate(nil)
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
修改设置
设置 LaunchHelp中的Application is Background Only : true
如果你使用的xcode14.3.1你可能也会遇到这个诡异问题内容显示不全,这里的info无法修改,value都看不到,可能是版本bug,也无法拖拉调整
不得已找到下图位置修改
设置LaunchHelper中Skill install:yes
在主项目中添加CopyFile到Contents/Library/LoginItems
完整完成上述操作,编译运行,主界面中勾选开机启动,之后重启系统看看是否生效
关于AppStore提交审核
苹果审核要求自启动应用,需要默认不开启,提供开启关闭选项让用户自己去决定是否使用。否则你的应用会被拒,违反的审核条例如下:
Guideline 2.4.5(iii) - Performance
Your app sets itself to auto-launch at startup without express consent from the user.
Specifically, Settings > Startup is pre-check marked.
Next Steps
You can resolve this by leaving this option unchecked by default, providing the user the option to turn it on.