导读:本期聚焦于半夏创作的《macOS开发入门:如何使用AppKit构建原生菜单栏应用并监听系统深色模式切换事件?》,敬请观看详情。想在macOS菜单栏上打造一款属于自己的常驻小工具吗?本文将以Swift和AppKit为基础,带你从零开始构建一个原生菜单栏应用,包括创建NSStatusItem、设计下拉菜单、隐藏Dock图标等核心步骤。随后深入讲解如何监听系统深色模式切换事件,利用NSApp.effectiveAppearance与 DistributedNotificationCenter两种方案实时响应用户在外观设置中的变化,并让应用图标和界面自动适配浅色与深色主题。文中还包含完整的示例代码、常见踩坑点分析以及NSAppearanceName的判断技巧,适合刚接触macOS开发的Swift开发者快速上手。

菜单栏应用是macOS平台上最具特色的应用形态之一,诸如iStat Menus、Bartender等知名工具都常驻于此。相比普通窗口应用,菜单栏应用占用屏幕空间极小,又能随时快速交互,非常适合做成状态监控、快捷工具类产品。本文将以Swift语言为基础,使用原生的AppKit框架,从零实现一个菜单栏应用,并在其基础上实现深色模式切换事件的监听,让应用界面能够随系统外观自动变化。

macOS开发入门:如何使用AppKit构建原生菜单栏应用并监听系统深色模式切换事件?

一、构建菜单栏应用的基础:NSStatusItem

AppKit中负责菜单栏图标的类是NSStatusBarNSStatusItemNSStatusBar是一个系统级的单例,通过它的statusItem(withLength:)方法可以申请一个菜单栏位置,返回的NSStatusItem对象就是应用在菜单栏中的载体。需要特别注意:必须使用强引用持有这个对象,否则它会被释放,图标随之消失。

NSStatusItem提供了一个NSStatusBarButton类型的button属性,可以像操作普通按钮一样设置标题或图标。图标通常使用NSImage(systemSymbolName:accessibilityDescription:)加载SF Symbols,这是苹果官方提供的矢量图标库,能自动适配深色模式,比自带的PNG资源更省心。下面是一段在AppDelegate中创建菜单栏项的完整示例:

import Cocoa

@main
class AppDelegate: NSObject, NSApplicationDelegate {

    // 必须强引用,否则菜单栏图标会被释放
    private var statusItem: NSStatusItem!

    func applicationDidFinishLaunching(_ notification: Notification) {
        // 创建固定长度的状态项
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)

        // 设置SF Symbols图标
        if let button = statusItem.button {
            button.image = NSImage(systemSymbolName: "moon.circle",
                                    accessibilityDescription: "主题指示")
        }

        buildMenu()
    }

    private func buildMenu() {
        let menu = NSMenu()
        menu.addItem(NSMenuItem(title: "关于本应用",
                                action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)),
                                keyEquivalent: ""))
        menu.addItem(.separator())
        menu.addItem(NSMenuItem(title: "退出",
                                action: #selector(NSApplication.terminate(_:)),
                                keyEquivalent: "q"))
        statusItem.menu = menu
    }
}

NSStatusItem设置menu属性后,点击菜单栏图标就会弹出下拉菜单,系统会自动处理高亮和关闭逻辑,无需手动管理。如果不使用菜单而是想监听点击事件,则应保持menu为nil,并通过button.actionbutton.target绑定回调,这两种交互方式不可混用,设置menu之后action将不再触发。

二、隐藏Dock图标,让应用成为纯菜单栏应用

默认情况下,macOS应用启动后会在Dock中显示图标,但对于菜单栏工具来说这是多余的。隐藏Dock图标最简单的办法是在Info.plist(或Xcode中Target的Information Property List设置)中添加一个键值:Application is agent (UI element),对应的键名为LSUIElement,类型为Boolean,值设为YES。

设置之后应用启动时既不会出现在Dock,也不会出现在应用切换器(Command+Tab)中,只在菜单栏留一个小图标,这正是状态类工具的标准形态。如果希望应用能在前台显示窗口时又出现在Dock,可以不在Info.plist中设置,而是在代码中动态调用:

import AppKit

// 恢复为普通应用(显示Dock图标)
NSApp.setActivationPolicy(.regular)

// 隐藏Dock图标,变成纯菜单栏应用
NSApp.setActivationPolicy(.accessory)

// 更彻底:连菜单栏都不参与,仅后台运行
NSApp.setActivationPolicy(.prohibited)

动态切换激活策略在某些需要临时弹出面板的场景很实用,例如点击菜单栏图标后弹出一个NSPopover面板,弹出时切换为regular,关闭时切回accessory。不过要注意频繁切换策略可能导致菜单栏焦点异常,多数情况下直接在Info.plist中配置LSUIElement是最稳妥的做法。

三、监听系统深色模式切换事件的两种方案

macOS从Mojave开始引入系统级深色模式,应用需要能实时感知用户在系统设置中的外观切换并做出响应。判断当前外观的第一步是读取NSApp.appearance或更可靠的NSApp.effectiveAppearance。前者是应用自己设置的外观,后者是实际生效的外观(会回退到系统设置),因此判断深浅色应始终使用effectiveAppearance

判断的具体做法是调用bestMatch(from:)方法,传入候选外观名称数组,系统会返回匹配度最高的那一个。不要用字符串直接比较name.rawValue是否等于dark,因为macOS还提供了高对比度外观等变体,bestMatch能正确处理这些情况。监听切换事件有两种主流方案,先看第一种,通过KVO监听effectiveAppearance

import Cocoa

extension NSAppearance.Name {
    /// 判断当前生效外观是否为深色
    var isDark: Bool {
        if let matched = NSApp.effectiveAppearance
            .bestMatch(from: [.darkAqua, .aqua]) {
            return matched == .darkAqua
        }
        return false
    }
}

class ThemeObserver: NSObject {

    private var observation: NSKeyValueObservation?

    override init() {
        super.init()
        // KVO监听整个NSApplication的生效外观
        observation = NSApplication.shared.observe(
            \.effectiveAppearance,
            options: [.new, .initial]
        ) { [weak self] _, change in
            let isDark = change.newValue?.name.isDark ?? false
            self?.applyTheme(isDark: isDark)
        }
    }

    private func applyTheme(isDark: Bool) {
        print("当前模式:\(isDark ? "深色" : "浅色")")
        // 在这里更新菜单栏图标或自定义视图配色
        if let button = (NSApp.delegate as? AppDelegate)?.statusItem.button {
            button.image = NSImage(
                systemSymbolName: isDark ? "moon.circle.fill" : "sun.max.circle",
                accessibilityDescription: nil)
        }
    }
}

第二种方案是利用DistributedNotificationCenter监听系统广播的外观切换通知。这种方式更底层,即便应用窗口尚未完全初始化也能收到事件,兼容一些需要跨进程感知外观变化的场景:

import Cocoa

class DistributedThemeObserver {

    private let center = DistributedNotificationCenter.default()

    func startObserving() {
        // 苹果内部通知,深色模式切换时系统会广播
        center.addObserver(
            self,
            selector: #selector(interfaceThemeChanged),
            name: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
            object: nil)
    }

    @objc private func interfaceThemeChanged() {
        // 通知不带具体值,需要自行读取UserDefaults确认当前主题
        let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
        print("外观已切换,深色模式:\(isDark)")
    }

    deinit {
        center.removeObserver(self)
    }
}

两种方案对比:KVO方式是官方推荐的标准做法,类型安全、代码现代(使用Swift Key Path),且能感知应用级外观覆盖(例如用户在Info.plist或代码中单独为应用设置了外观);分布式通知方式胜在通知时机更早、跨进程可用,但通知名称属于内部约定,读取AppleInterfaceStyle这个UserDefaults键值也并非公开API文档中的正式接口,存在小概率变动风险。实际项目中建议优先采用KVO方案,分布式通知作为补充。

四、深色模式适配的常见坑与最佳实践

第一个常见的坑是自定义绘制颜色时写死了具体色值。解决方式是尽量使用系统语义化颜色,例如NSColor.controlTextColorNSColor.windowBackgroundColor,它们会在深浅模式下自动变化。确实需要自定义颜色时,应使用Asset Catalog中的颜色资源,并在Xcode的Appearances选项中选择Any, Dark,分别提供两套色值。

第二个坑是模板图标没有启用。菜单栏图标设置后如果出现一个黑色方块或显示异常,多半是没有调用image.isTemplate = true。设置为模板图后,系统会自动根据菜单栏的深浅背景渲染图标的黑白颜色,这与SF Symbols的默认行为一致,是菜单栏图标的标准实践。

if let button = statusItem.button {
    let image = NSImage(named: "custom-icon")
    image?.isTemplate = true  // 关键:让系统自动适配深浅色
    button.image = image
}

第三个坑是KVO回调线程问题。effectiveAppearance的变化回调一般发生在主线程,但如果应用中存在其他后台组件触发了外观刷新,保险起见可以在回调中用DispatchQueue.main.async包裹UI更新逻辑。此外,如果使用SwiftUI与AppKit混编,也可以在SwiftUI视图层用@Environment(\.colorScheme)获取外观,但在纯AppKit的菜单栏应用中,KVO监听NSApp.effectiveAppearance仍是最直接可靠的方式。掌握这些要点后,你就可以构建出一个外观随系统自动切换、交互规范的原生菜单栏应用了。

macOS开发AppKit深色模式监听修改时间:2026-09-01 20:30:44

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。