案例分析
在SwiftUI中,我想要通过修改配置同步修改状态栏。
class AppStorage:ObservableObject {
static var shared = AppStorage()
// 菜单栏显示图标,true为显示
@Published var displayMenuBarIcon = true
}
当我修改displayMenuBarIcon变量后,移除macOS的状态栏。

但是状态栏的生命周期是通过AppDelegate管理的:
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarController: StatusBarController?
func applicationDidFinishLaunching(_ notification: Notification) {
// 根据设置中的菜单栏选项,创建菜单栏
if AppStorage.shared.displayMenuBarIcon {
statusBarController = StatusBarController()
}
}
}
class StatusBarController:ObservableObject {
private var statusItem: NSStatusItem?
init() {
// 创建系统菜单栏图标
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
}
}
如果说SwiftUI的场景,可以使用@StateObject / @ObservedObject 自动监听 ObservableObject。
在非SwiftUI的上下文(比如AppDelegate)里,就需要主动使用Combine去订阅 @Published 生成的 Publisher。
@Published 实际上生成的是一个 Publisher:可以通过 $propertyName 获取到它(比如 $displayMenuBarIcon)。每次更新 @Published,都会自动发送新值给所有订阅者(包括 AppDelegate 中的订阅)。
使用Combine
在AppStorage中,@Published是Combine的发布者,会自动生成一个Publisher。
AppDelegate监听AppStorage.shared.$displayMenuBarIcon(Combine 订阅),控制 statusBarController 的创建与销毁。

1、在StatusBarController添加移除菜单栏图标的方法:
class StatusBarController {
private var statusItem: NSStatusItem!
init() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
}
// 添加移除方法
func removeFromStatusBar() {
if let item = statusItem {
NSStatusBar.system.removeStatusItem(item)
statusItem = nil
}
}
}
通过调用StatusBarController的removeFromStatusBar方法,移除菜单栏图标。
2、在 AppDelegate 中添加Combine 订阅支持:
import Combine
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarController: StatusBarController?
var cancellables = Set<AnyCancellable>()
func applicationDidFinishLaunching(_ notification: Notification) {
// 初始化时根据设置显示菜单栏图标
if AppStorage.shared.displayMenuBarIcon {
statusBarController = StatusBarController()
}
// 监听 displayMenuBarIcon 的变化
AppStorage.shared.$displayMenuBarIcon
.receive(on: RunLoop.main)
.sink { [weak self] showIcon in
guard let self = self else { return }
if showIcon {
if self.statusBarController == nil {
self.statusBarController = StatusBarController()
}
} else {
self.statusBarController?.removeFromStatusBar()
self.statusBarController = nil
}
}
.store(in: &cancellables)
// 初始化主窗口逻辑 ...
}
}
3、代码解析:
关键Combine代码:
AppStorage.shared.$displayMenuBarIcon
.receive(on: RunLoop.main)
.sink { [weak self] showIcon in
...
}
.store(in: &cancellables)
1)AppStorage.shared.$displayMenuBarIcon
$displayMenuBarIcon 是 Combine 提供的投影属性(projected value)。
因为它是用 @Published 声明的,所以系统自动提供一个 Publisher(类型为 Published<Bool>.Publisher)。
每当 displayMenuBarIcon 的值发生变化,Publisher 就会发出新值(Bool)。
订阅的其实是这个“Publisher”,不是值本身。

2).receive(on: RunLoop.main)
指定接收数据的线程/调度器。
因为在回调里更新 UI(创建或销毁 status bar 控制器),所以必须回到主线程执行。
关于.recive(on:)相关知识,可以查看《Apple处理异步任务的Combine框架》的文章底部扩展知识的“.receive(on:)”部分。
3).sink { [weak self] showIcon in … }
.sink 是最常用的订阅者(Subscriber)方法,用于对 Publisher 发出的值做出响应。
showIcon 是每次值变化时的新值(Bool)。
[weak self] 防止 AppDelegate 和闭包之间造成强引用循环(内存泄漏)。
4).store(in: &cancellables)
sink 返回一个 AnyCancellable,表示一个“订阅关系”。
为了保持订阅有效,必须把它存储起来。
一旦 cancellables 被释放(或手动取消),订阅就会停止,回调不再被调用。
总结
在Swift中使用Combine监听,可以实现动态管理菜单栏的功能。
首先是在管理的单例中使用ObservableObject + @Published,创建Combine发布者,每次更新@Published变量都会自动发送新值。
在AppDelegate的生命周期中,设置AppStorage.shared.$displayMenuBarIcon获取Combine的投影属性,利用 .sink 接收 @Publsihed 发送的新值。
最后根据值决定创建/移除菜单栏图标,同时确保UI都在主线程中执行(确保安全)。
相关文章
Apple处理异步任务的Combine框架:https://fangjunyu.com/2024/12/01/apple%e5%a4%84%e7%90%86%e5%bc%82%e6%ad%a5%e4%bb%bb%e5%8a%a1%e7%9a%84combine%e6%a1%86%e6%9e%b6/
扩展知识
为什么 var cancellables = Set<AnyCancellable>() 是 Set 类型?
因为可能会有多个订阅:
AnyCancellable 表示一个具体的订阅(比如你用 .sink 创建的订阅)。
在一个真实项目中,很可能会订阅多个 Publisher,每个都会返回一个 AnyCancellable。
如果只用一个变量去存储,后一个会覆盖前一个,导致前一个订阅被取消。
使用Set<AnyCancellable>可以统一管理多个订阅:
var cancellables = Set<AnyCancellable>()
publisher1
.sink { ... }
.store(in: &cancellables)
publisher2
.sink { ... }
.store(in: &cancellables)
当 cancellables 被释放或清空时,里面所有的订阅都会自动取消。
这是 Combine 的自动资源释放机制的一部分。
.store(in: &cancellables) 是“绑定”吗?
不是“绑定”概念,更准确说是“持有”(retain)这个订阅对象。
.store(in: &cancellables) 会将 AnyCancellable 插入 Set 中,防止其在作用域结束后被释放,从而保持订阅持续有效。
其他监听方式
在非SwiftUI的上下文(比如AppDelegate)里,除了Combine,还有其他的监听方式么?
实际上有下面几种可选方案:
1、KVO(Key-Value Observing)
可用于 Objective-C 或继承自 NSObject 的类,但不推荐用于 Swift 原生 struct/class 的属性。
object.addObserver(self, forKeyPath: "value", options: [.new], context: nil)
不推荐,因为它对 Swift 类型兼容性差、语法冗长、易出错。
2、KVO 的 Swift 包装方式(通过 @objc dynamic)
如果把 AppStorage 设计成继承自 NSObject,再加 @objc dynamic 修饰属性,是可以用 KVO 的:
@objc dynamic var displayMenuBarIcon: Bool = true
但这不是纯 Swift 做法,而且 KVO 没有 Combine 的声明式、链式语法优雅。
3、NotificationCenter
可以在属性改变时手动发送通知,在需要监听的地方订阅:
// 发出通知
NotificationCenter.default.post(name: .didChangeDisplayMenuBarIcon, object: nil)
// 监听
NotificationCenter.default.addObserver(forName: .didChangeDisplayMenuBarIcon, ...)
适合跨模块通信,但没有类型安全,写起来繁琐、容易错。
4、自定义闭包回调机制
这是最“原始”的方式:
class AppStorage {
var displayMenuBarIcon: Bool = true {
didSet {
onDisplayMenuBarIconChanged?(displayMenuBarIcon)
}
}
var onDisplayMenuBarIconChanged: ((Bool) -> Void)?
}
可控性强,适合小场景,但扩展性差、可读性低,不如 Combine。
在这些监听方式中,Combine类型安全、语法优雅、支持链式、生命周期易管理,是 Swift 现代推荐方式。