本文へ移動
cccskills
無料GitHub で公開

macos-notifications

Use when adding or reviewing macOS user notifications — UNUserNotificationCenter permission flow, categories and action buttons, click routing through the delegate, foreground presentation, and rules that keep notifications from becoming spam.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md9.2 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

macOS Notifications

Notify only when the user benefits from being pulled back — task finished, task failed, something needs their decision — and make every notification click land on the exact screen that resolves it.

When to use

Use this skill when the user says:

  • notify when done / notification when the task finishes
  • desktop notification / system notification
  • UNUserNotificationCenter / UserNotifications
  • notification permission
  • notification actions / buttons
  • clicking the notification should open …
  • background job finished, tell the user
  • reminder from a macOS app

Do not use this skill for push/APNs server setup, iOS-specific notification UI, or in-window status banners (those are in-app feedback — see apple-hig-feedback-status).

Core rule

Notify only when being pulled back saves the user something.
Every notification answers: what happened, and what happens
when I click it.

When to notify — and when not

Worth a notification (the app is likely in the background):

Completed     long task finished        → click opens the result
Failed        long task failed          → click opens the error + retry
Blocked       waiting on input/approval → click opens the decision
Needs review  output ready to check     → click opens the review view
Reminder      user-scheduled            → click opens the item

Not worth one:

  • Progress ticks, intermediate states, "still working…"
  • Anything the user is currently looking at (foreground, same screen)
  • Marketing, tips, "did you know" — never
  • Errors the app can recover from by itself

If the app fires more than a handful of notifications an hour, the model is wrong — batch them (one summary notification) or drop the low-value ones.

Permission

Request at the moment of need, with context, never at first launch:

import UserNotifications

enum Notifier {
    // Call right after the user does the thing that will later notify —
    // e.g. starts their first long-running task.
    static func requestPermission() async -> Bool {
        let center = UNUserNotificationCenter.current()
        do {
            return try await center.requestAuthorization(options: [.alert, .sound, .badge])
        } catch {
            return false
        }
    }

    // Never assume — settings can change behind the app's back.
    static func isAuthorized() async -> Bool {
        let settings = await UNUserNotificationCenter.current().notificationSettings()
        return settings.authorizationStatus == .authorized
                || settings.authorizationStatus == .provisional
    }
}

Rules:

  • Pre-explain. Before the system prompt, the UI has already made the value obvious ("Runs can take a while — want a notification when one finishes?"). The system dialog should never be a surprise.
  • The system prompt fires once. If the user denies, requestAuthorization won't re-prompt — your only path is Settings, so keep a "notifications are off" state in your own Settings pane with a deep link:
// Opens System Settings > Notifications
NSWorkspace.shared.open(
    URL(string: "x-apple.systempreferences:com.apple.Notifications-Settings.extension")!)
  • Consider .provisional for low-stakes apps: notifications deliver quietly to Notification Center without any prompt, and the user promotes or demotes them from there.

Sending, with categories and actions

Define categories once at launch; they give notifications buttons and give clicks a routable identity:

enum TaskNotification {
    static let category = "task.finished"
    static let retryAction = "task.retry"

    static func registerCategories() {
        let retry = UNNotificationAction(identifier: retryAction,
                                         title: "Retry",
                                         options: [])
        let category = UNNotificationCategory(identifier: category,
                                              actions: [retry],
                                              intentIdentifiers: [])
        UNUserNotificationCenter.current().setNotificationCategories([category])
    }

    static func post(taskID: String, title: String, body: String) async {
        guard await Notifier.isAuthorized() else { return }

        let content = UNMutableNotificationContent()
        content.title = title                       // "Export failed"
        content.body = body                         // "3 of 120 items didn't convert."
        content.sound = .default
        content.categoryIdentifier = category
        content.userInfo = ["taskID": taskID]       // routing payload
        content.threadIdentifier = "tasks"          // groups related ones

        let request = UNNotificationRequest(identifier: taskID,   // same ID = replaces
                                            content: content,
                                            trigger: nil)         // deliver now
        try? await UNUserNotificationCenter.current().add(request)
    }
}

Details that matter:

  • Title = what happened; body = the one detail that matters. No app name in the title (the system shows it), no exclamation marks.
  • Reusing the request identifier replaces the previous notification — the right behavior for a task that goes blocked → completed.
  • threadIdentifier keeps a burst of task notifications stacked instead of carpeting the screen.
  • Failure notifications include the recovery path — a Retry action button, and a click target that shows the error.

Click routing — the delegate

Without a delegate, clicks just activate the app and notifications hide while the app is frontmost. Set it early (before any notification can arrive):

import AppKit
import UserNotifications

final class AppDelegate: NSObject, NSApplicationDelegate,
                         UNUserNotificationCenterDelegate {

    func applicationDidFinishLaunching(_ notification: Notification) {
        UNUserNotificationCenter.current().delegate = self
        TaskNotification.registerCategories()
    }

    // Route the click to the exact screen that resolves it.
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse) async {
        let info = response.notification.request.content.userInfo
        guard let taskID = info["taskID"] as? String else { return }

        switch response.actionIdentifier {
        case TaskNotification.retryAction:
            await TaskStore.shared.retry(taskID)
        default:                                    // plain click
            NSApp.activate(ignoringOtherApps: true)
            Router.shared.open(.taskDetail(taskID))
        }
    }

    // Decide what happens if the app is frontmost when one arrives.
    // Default: show nothing. Menu bar / background-style apps usually
    // still want the banner; window apps showing that screen don't.
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification) async
                                -> UNNotificationPresentationOptions {
        [.banner, .sound]
    }
}

// SwiftUI wiring:
@main
struct MyApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene { /* … */ }
}

Also clean up after yourself — when the user opens the screen a notification points at, remove it from Notification Center:

UNUserNotificationCenter.current()
    .removeDeliveredNotifications(withIdentifiers: [taskID])

Settings pane

Give users per-type control inside the app, on top of the system switch:

Notifications
  Notify me when:
    [x] A run completes
    [x] A run fails or is blocked
    [ ] A daily summary is ready
  Notifications are disabled in System Settings.  Open Settings…   (only when denied)

Check the per-type toggles in your own post() path — isAuthorized() handles the system layer, these handle taste.

Checklist

[ ] Permission requested at the moment of need, with a pre-explanation
[ ] Denied state visible in app Settings with a System Settings deep link
[ ] Delegate set in applicationDidFinishLaunching, categories registered
[ ] Every notification click opens the exact relevant screen
[ ] Failure notifications carry a recovery action (Retry / Show error)
[ ] Identifiers reused so state changes replace, not stack
[ ] threadIdentifier groups bursts; no notification-per-tick spam
[ ] Delivered notifications removed once the user has seen the thing
[ ] Per-type toggles in app Settings

Manual test

  1. Fresh install → trigger the notifying action → pre-explanation, then system prompt.
  2. Background the app, finish a task → notification arrives; click → app opens on that task.
  3. Fail a task → notification has Retry; Retry works without opening the app.
  4. Deny permission in System Settings → app Settings shows the off state + working deep link.
  5. Fire 5 task notifications → they stack as one group, latest state per task wins.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use when creating a new project folder, starter pack, app scaffold, or agent-ready workspace — AGENTS.md, README, TASKS, DECISIONS, PROMPTS, an idempotent bootstrap script, and starter-pack modes.

日本語の概要は準備中です。原文の説明を表示しています。

aka-kika/akakika-skills112026年9月12日 更新

Use when designing or implementing states for AI agent tasks, background jobs, local model runs, queues, and notifications — one shared set of states, only legal transitions, and a recovery path out of every failure.

日本語の概要は準備中です。原文の説明を表示しています。

aka-kika/akakika-skills112026年9月12日 更新

Use when exposing app features to App Intents, Shortcuts, Siri, Spotlight, widgets, controls, or system actions — small safe AppIntents, AppShortcutsProvider, AppEnum/AppEntity, parameters, and open-vs-background behavior.

日本語の概要は準備中です。原文の説明を表示しています。

aka-kika/akakika-skills112026年9月12日 更新

Use when designing, reviewing, or implementing a command-palette / ⌘K layer for an Apple-platform app — keyboard-first global search, ranked and grouped results, native floating panel, and protected destructive commands.

日本語の概要は準備中です。原文の説明を表示しています。

aka-kika/akakika-skills112026年9月12日 更新

Use when designing or implementing empty, first-run, no-result, offline, error, or waiting states in Apple-platform apps — every empty state explains what is missing, why it matters, and what to do next.

日本語の概要は準備中です。原文の説明を表示しています。

aka-kika/akakika-skills112026年9月12日 更新

Use when designing or implementing feedback, status, progress, alerts, errors, success states, and live activity indicators in Apple-platform apps — inline status, banners, alerts, notifications, and loading states.

日本語の概要は準備中です。原文の説明を表示しています。

aka-kika/akakika-skills112026年9月12日 更新

aka-kika のスキルをすべて見る

このスキルの問題を報告する