LoginSignup
6
6

macOSアプリでLaunchAtLoginの実装

Last updated at Posted at 2022-01-31

LaunchAtLoginの実装

追記(2024-03-16)


概要

  • アプリでよくあるLaunch at Loginの実装を行う。
  • LaunchAtLoginの仕組みとしてはざっくり以下の通り。
    • 起動したいアプリ(以下メインアプリ)にヘルパーアプリを内包させる。
    • ヘルパーアプリをユーザログイン時に起動するよう登録。
    • (ユーザログイン時)ヘルパーアプリからメインアプリを起動させ、ヘルパーアプリを終了させる。
image

GitHub

参考

NSWorkspace.shared.urlForApplication()で本体アプリのファイルパスを取得する。

プロジェクト設定

メインアプリ

  • メインのアプリの作成

  • 以下からヘルパーアプリを作成

  • メインアプリにヘルパーアプリを内包させる設定

  • ServiceManagement.frameworkを追加

ヘルパーアプリ

  • Build SettingsSkip InstallYES

  • Info.plistApplication is background onlyを追加しValueをYESにする

  • ヘルパーアプリ側のUIがいらないので下記を削除

  • Main storyboard file base nameMainを空にする

実装

メインアプリ

LauncherConst.swift

  • ヘルパーアプリのIdentifierを複数箇所で使用するので下記の通り宣言しておく。
import Foundation

struct LauncherConst {
    static let launcherAppId = "com.gmail.ikeh1024.MainApplicationLauncher"
}

AppDelegate.swift

  • 起動時にヘルパーアプリが起動している場合に、ヘルパーアプリを終了させるようにする。
  • DistributedNotificationCenterで異なるアプリ間でやりとりできる。
import Cocoa
import ServiceManagement

extension Notification.Name {
    static let killLauncher = Notification.Name("killLauncher")
}

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // ヘルパーが起動していたら終了させる
        let runningApps = NSWorkspace.shared.runningApplications
        let isRunning = !runningApps.filter { $0.bundleIdentifier == LauncherConst.launcherAppId }.isEmpty
        if isRunning {
            DistributedNotificationCenter.default().post(name: .killLauncher, object: Bundle.main.bundleIdentifier!)
        }
    }
...
}

ViewContorller.swift

  • 下記コードはチェックボックスを選択したときのアクション。
  • SMLoginItemSetEnabled(_:_:)でヘルパーアプリのIdentifierを登録することで、ログイン時にヘルパーアプリが起動される。
@IBAction func launchAtLoginCheckboxClicked(_ sender: NSButton) {
    SMLoginItemSetEnabled(LauncherConst.launcherAppId as CFString, GeneralPreferences.shared.launchAtLogin.isOn)
}
  • Launch at Loginのチェックボックスは、CocoaBindingを使い、UserDefaultsのlaunchAtLoginをキーとして同期させている。

ヘルパーアプリ

main.swift

  • エントリーポイントとしての実装
import AppKit

let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()

AppDelegate.swift

  • メインアプリの起動に関する設定を諸々行う
import Cocoa

extension Notification.Name {
    static let killLauncher = Notification.Name("killLauncher")
}

class AppDelegate: NSObject {

    @objc func terminate() {
        NSApp.terminate(nil)
    }
}

extension AppDelegate: NSApplicationDelegate {

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        // メインアプリの起動確認
        let mainAppIdentifier = "com.gmail.ikeh1024.MainApplication"
        let runningApps = NSWorkspace.shared.runningApplications
        let isRunning = !runningApps.filter { $0.bundleIdentifier == mainAppIdentifier }.isEmpty
        
        if !isRunning {
            // メインアプリ起動時に終了させる通知を受け取るための設定
            DistributedNotificationCenter
                .default()
                .addObserver(self,
                             selector: #selector(self.terminate),
                             name: .killLauncher,
                             object: mainAppIdentifier)

            // メインアプリを起動する
            if !isRunning,
               let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: mainAppIdentifier) {
                let config = NSWorkspace.OpenConfiguration()
                NSWorkspace.shared.openApplication(at: url, configuration: config) { _, _ in
                }
            }
        }
        else {
            // メインアプリが起動していれば何もせずに終了
            self.terminate()
        }
    }
}

ヘルパーアプリが登録されているかの確認方法

@IBAction func checkSMLoginButtonClicked(_ sender: Any) {
    let jobDicts = SMCopyAllJobDictionaries(kSMDomainUserLaunchd).takeRetainedValue() as NSArray as! [[String:AnyObject]]
    let jobEnabled = jobDicts.filter { $0["Label"] as! String == LauncherConst.launcherAppId }.isEmpty == false
    print("SMLogin: \(jobEnabled)")
}

SMLoginItemSetEnabled(...) GET counterpart
On SMCopyAllJobDictionaries inside For the specific use of testing the state of a login item that may have been enabled with SMLoginItemSetEnabled() in order to show that state to the user, this function remains the recommended API. A replacement API for this specific use will be provided before this function is removed. We can safely use the method until further notice. Just be sure to check the new SDK releases/updates for the new API and this method's obsolescence. –
Nogurenn
Feb 7 '19 at 8:15

  • 実際にXcode上で覗いてみるとlibxpcで置き換わる予定と記載があった。
@available(macOS, introduced: 10.6, deprecated: 10.10)
public func SMCopyAllJobDictionaries(_ domain: CFString!) -> Unmanaged<CFArray>!

/**
(省略)
 * @discussion
 * This routine is deprecated and will be removed in a future release. A 
 * replacement will be provided by libxpc.
 */
6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6