概要
現在開発中の Mac アプリで、定期実行と実行完了時のローカル Push 通知を実装したので、その内容をまとめる。
今回作ったもの
以下の UI で実行時間を設定する。

Mac アプリ上でスケジューラに登録し、指定した時間に処理を実行。完了後はローカル Push 通知を表示するように実装した。

今回はセキュリティの観点から、サーバー側で定期実行するのではなく、Mac アプリ自身が定期実行する 方式にしている。
Swiftでスケジューラを登録する
Swift では Timer.scheduledTimerを使うことでスケジュール実行できる。
JavaScript の setTimeout や setInterval に近い感覚で使える。
import Foundation var timer: Timer? timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in print("1秒後に実行") }
実装
スケジューラのクラスを作成
■ AutoFetchScheduler.swift
import Foundation /// 一定間隔で「データを更新」を呼ぶだけのシンプルなスケジューラ final class AutoFetchScheduler { static let shared = AutoFetchScheduler() private var timer: Timer? private var settings: AutoFetchSettings? // スケジュールで実行したい処理を設定 var fetchHandler: (() -> Void)? private func scheduleNext() { // 次回の実行日時を取得してスケジューラに登録 let delay = max(nextFireDate().timeIntervalSinceNow, 1) timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in self?.fireAndReschedule() } RunLoop.main.add(timer!, forMode: .common) } private func fireAndReschedule() { // 処理実行後に次のスケジューラを登録 defer { scheduleNext() } // 処理実行 fetchHandler?() } (略) }
スケジューラで実行したい内容を外部から設定する
■ AppDelegate
/// macOSアプリ起動時の処理を担当 class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { func applicationDidFinishLaunching(_: Notification) { AutoFetchScheduler.shared.fetchHandler = { [weak self] in self?.runUpdateBinary() // ← ここにスケジューラで実行したい処理を設定 } } }
Macがスリープしていた時の挙動を実装する
Timer.scheduledTimer は Mac がスリープ中だとタイマー自体が動作しない。
そのため、スリープ解除時に本来の実行予定時刻を過ぎていたら、そのタイミングで実行する 仕様で実装した。
■ AutoFetchScheduler.swift
final class AutoFetchScheduler { (略) /// スリープ解除時に呼ぶ。1スロット以上経過していれば即実行してタイマーを再設定する func wakeFromSleep() { guard let settings = settings else { return } // タイマーをリセット(スリープ中に発火予定時刻が過ぎている可能性があるため) stop() if let last = lastExecutedAt { let elapsed = Date().timeIntervalSince(last) if elapsed >= settings.timeInterval, ignoreTime() == false, checkWeekday() == true { lastExecutedAt = Date() fetchHandler?() } } scheduleNext() } (略) }
Swift でスリープ解除を検知するには NSWorkspace.shared.notificationCenter.addObserver を使う。
■ AppDelegate
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { func applicationDidFinishLaunching(_: Notification) { // スリープ解除を検知 NSWorkspace.shared.notificationCenter.addObserver( self, selector: #selector(onWakeFromSleep), name: NSWorkspace.didWakeNotification, object: nil ) } @objc private func onWakeFromSleep() { AutoFetchScheduler.shared.wakeFromSleep() } }
ここまででスケジューラの実装は完了。
Push通知の権限をリクエストする
UNUserNotificationCenter.current().requestAuthorization を使って Push 通知の許可をリクエストする。
■ AppDelegate
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { func applicationDidFinishLaunching(_: Notification) { // Push通知の許可をリクエスト UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in NSLog("通知権限: granted=\(granted), error=\(String(describing: error))") } } }
Push通知の権限がOFFの場合の表示
Push 通知の権限が OFF の場合は以下のような表示を出す。

UNUserNotificationCenter.current().getNotificationSettings で権限状態を取得できるため、OFF の場合に表示を追加する。
private func checkNotificationPermission() { UNUserNotificationCenter.current().getNotificationSettings { settings in DispatchQueue.main.async { notificationDenied = settings.authorizationStatus == .denied } } }
ローカルPush通知を登録する
UNUserNotificationCenter.current().add(request) でローカル Push 通知を登録できる。
スケジューラの実行完了後に以下の sendUpdateNotification を呼べば完了。
■ AppDelegate
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { private func sendUpdateNotification() { let formatter = DateFormatter() formatter.dateFormat = "yyyy/MM/dd HH:mm" let dateStr = formatter.string(from: Date()) let content = UNMutableNotificationContent() content.title = "自動更新完了" content.body = "\(dateStr)で更新" content.sound = .default let request = UNNotificationRequest( identifier: UUID().uuidString, content: content, trigger: nil ) UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) } }
これで以下のようにローカル Push 通知が表示される。

まとめ
- 常駐型の Mac アプリを作るうえで、スケジューラ + ローカル Push 通知はかなり使いどころの多い機能
Timer.scheduledTimerをベースにしつつ、スリープ復帰時の補正を入れることで実用的に動かせた