본문으로 건너뛰기
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.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

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의 스킬 모두 보기

이 스킬의 문제 신고