继续解读下面的相关代码:
//
// AppState.swift
// Ice
//
import Combine
import SwiftUI
/// The model for app-wide state.
@MainActor
final class AppState: ObservableObject {
/// A Boolean value that indicates whether the active space is fullscreen.
@Published private(set) var isActiveSpaceFullscreen = Bridging.isSpaceFullscreen(Bridging.activeSpaceID)
/// Manager for the menu bar's appearance.
private(set) lazy var appearanceManager = MenuBarAppearanceManager(appState: self)
/// Manager for events received by the app.
private(set) lazy var eventManager = EventManager(appState: self)
/// Manager for menu bar items.
private(set) lazy var itemManager = MenuBarItemManager(appState: self)
/// Manager for the state of the menu bar.
private(set) lazy var menuBarManager = MenuBarManager(appState: self)
/// Manager for app permissions.
private(set) lazy var permissionsManager = PermissionsManager(appState: self)
/// Manager for the app's settings.
private(set) lazy var settingsManager = SettingsManager(appState: self)
/// Manager for app updates.
private(set) lazy var updatesManager = UpdatesManager(appState: self)
/// Manager for user notifications.
private(set) lazy var userNotificationManager = UserNotificationManager(appState: self)
/// Global cache for menu bar item images.
private(set) lazy var imageCache = MenuBarItemImageCache(appState: self)
/// Manager for menu bar item spacing.
let spacingManager = MenuBarItemSpacingManager()
/// Model for app-wide navigation.
let navigationState = AppNavigationState()
/// The app's hotkey registry.
nonisolated let hotkeyRegistry = HotkeyRegistry()
/// The app's delegate.
private(set) weak var appDelegate: AppDelegate?
/// The window that contains the settings interface.
private(set) weak var settingsWindow: NSWindow?
/// The window that contains the permissions interface.
private(set) weak var permissionsWindow: NSWindow?
/// A Boolean value that indicates whether the "ShowOnHover" feature is prevented.
private(set) var isShowOnHoverPrevented = false
/// Storage for internal observers.
private var cancellables = Set<AnyCancellable>()
/// A Boolean value that indicates whether the app is running as a SwiftUI preview.
let isPreview: Bool = {
#if DEBUG
let environment = ProcessInfo.processInfo.environment
let key = "XCODE_RUNNING_FOR_PREVIEWS"
return environment[key] != nil
#else
return false
#endif
}()
/// A Boolean value that indicates whether the application can set the cursor
/// in the background.
var setsCursorInBackground: Bool {
get { Bridging.getConnectionProperty(forKey: "SetsCursorInBackground") as? Bool ?? false }
set { Bridging.setConnectionProperty(newValue, forKey: "SetsCursorInBackground") }
}
/// Configures the internal observers for the app state.
private func configureCancellables() {
var c = Set<AnyCancellable>()
Publishers.Merge3(
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.activeSpaceDidChangeNotification)
.mapToVoid(),
// Frontmost application change can indicate a space change from one display to
// another, which gets ignored by NSWorkspace.activeSpaceDidChangeNotification.
NSWorkspace.shared
.publisher(for: \.frontmostApplication)
.mapToVoid(),
// Clicking into a fullscreen space from another space is also ignored.
UniversalEventMonitor
.publisher(for: .leftMouseDown)
.delay(for: 0.1, scheduler: DispatchQueue.main)
.mapToVoid()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else {
return
}
isActiveSpaceFullscreen = Bridging.isSpaceFullscreen(Bridging.activeSpaceID)
}
.store(in: &c)
NSWorkspace.shared.publisher(for: \.frontmostApplication)
.receive(on: DispatchQueue.main)
.sink { [weak self] frontmostApplication in
guard let self else {
return
}
navigationState.isAppFrontmost = frontmostApplication == .current
}
.store(in: &c)
if let settingsWindow {
settingsWindow.publisher(for: \.isVisible)
.debounce(for: 0.05, scheduler: DispatchQueue.main)
.sink { [weak self] isVisible in
guard let self else {
return
}
navigationState.isSettingsPresented = isVisible
}
.store(in: &c)
} else {
Logger.appState.warning("No settings window!")
}
Publishers.Merge(
navigationState.$isAppFrontmost,
navigationState.$isSettingsPresented
)
.debounce(for: 0.1, scheduler: DispatchQueue.main)
.sink { [weak self] shouldUpdate in
guard
let self,
shouldUpdate
else {
return
}
Task.detached {
if ScreenCapture.cachedCheckPermissions(reset: true) {
await self.imageCache.updateCacheWithoutChecks(sections: MenuBarSection.Name.allCases)
}
}
}
.store(in: &c)
menuBarManager.objectWillChange
.sink { [weak self] in
self?.objectWillChange.send()
}
.store(in: &c)
permissionsManager.objectWillChange
.sink { [weak self] in
self?.objectWillChange.send()
}
.store(in: &c)
settingsManager.objectWillChange
.sink { [weak self] in
self?.objectWillChange.send()
}
.store(in: &c)
updatesManager.objectWillChange
.sink { [weak self] in
self?.objectWillChange.send()
}
.store(in: &c)
cancellables = c
}
/// Sets up the app state.
func performSetup() {
configureCancellables()
permissionsManager.stopAllChecks()
menuBarManager.performSetup()
appearanceManager.performSetup()
eventManager.performSetup()
settingsManager.performSetup()
itemManager.performSetup()
imageCache.performSetup()
updatesManager.performSetup()
userNotificationManager.performSetup()
}
/// Assigns the app delegate to the app state.
func assignAppDelegate(_ appDelegate: AppDelegate) {
guard self.appDelegate == nil else {
Logger.appState.warning("Multiple attempts made to assign app delegate")
return
}
self.appDelegate = appDelegate
}
/// Assigns the settings window to the app state.
func assignSettingsWindow(_ window: NSWindow) {
guard window.identifier?.rawValue == Constants.settingsWindowID else {
Logger.appState.warning("Window \(window.identifier?.rawValue ?? "<NIL>") is not the settings window!")
return
}
settingsWindow = window
configureCancellables()
}
/// Assigns the permissions window to the app state.
func assignPermissionsWindow(_ window: NSWindow) {
guard window.identifier?.rawValue == Constants.permissionsWindowID else {
Logger.appState.warning("Window \(window.identifier?.rawValue ?? "<NIL>") is not the permissions window!")
return
}
permissionsWindow = window
configureCancellables()
}
/// Opens the settings window.
func openSettingsWindow() {
with(EnvironmentValues()) { environment in
environment.openWindow(id: Constants.settingsWindowID)
}
}
/// Dismisses the settings window.
func dismissSettingsWindow() {
with(EnvironmentValues()) { environment in
environment.dismissWindow(id: Constants.settingsWindowID)
}
}
/// Opens the permissions window.
func openPermissionsWindow() {
with(EnvironmentValues()) { environment in
environment.openWindow(id: Constants.permissionsWindowID)
}
}
/// Dismisses the permissions window.
func dismissPermissionsWindow() {
with(EnvironmentValues()) { environment in
environment.dismissWindow(id: Constants.permissionsWindowID)
}
}
/// Activates the app and sets its activation policy to the given value.
func activate(withPolicy policy: NSApplication.ActivationPolicy) {
// Store whether the app has previously activated inside an internal
// context to keep it isolated.
enum Context {
static let hasActivated = ObjectStorage<Bool>()
}
func activate() {
if let frontApp = NSWorkspace.shared.frontmostApplication {
NSRunningApplication.current.activate(from: frontApp)
} else {
NSApp.activate()
}
NSApp.setActivationPolicy(policy)
}
if Context.hasActivated.value(for: self) == true {
activate()
} else {
Context.hasActivated.set(true, for: self)
Logger.appState.debug("First time activating app, so going through Dock")
// Hack to make sure the app properly activates for the first time.
NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.activate()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
activate()
}
}
}
/// Deactivates the app and sets its activation policy to the given value.
func deactivate(withPolicy policy: NSApplication.ActivationPolicy) {
if let nextApp = NSWorkspace.shared.runningApplications.first(where: { $0 != .current }) {
NSApp.yieldActivation(to: nextApp)
} else {
NSApp.deactivate()
}
NSApp.setActivationPolicy(policy)
}
/// Prevents the "ShowOnHover" feature.
func preventShowOnHover() {
isShowOnHoverPrevented = true
}
/// Allows the "ShowOnHover" feature.
func allowShowOnHover() {
isShowOnHoverPrevented = false
}
}
// MARK: AppState: BindingExposable
extension AppState: BindingExposable { }
// MARK: - Logger
private extension Logger {
/// The logger to use for the app state.
static let appState = Logger(category: "AppState")
}
好的,我们继续解读 AppState.swift
文件。
如果说 AppDelegate
是应用程序的“事件总管”,那么 AppState
就是整个应用程序的**“大脑”和“中央数据中心”**。它持有并管理着 app 运行所需的所有状态、数据和核心逻辑。AppDelegate
负责响应系统事件,然后通知 AppState
来进行实际的状态更新和处理。
核心角色与职责
这个 AppState.swift
文件定义了一个名为 AppState
的类,它的主要职责是:
- 状态管理 (State Management):作为**“唯一事实来源 (Single Source of Truth)”**,集中存储整个 app 的状态,比如当前是否全屏、设置项的值等。
- 模块协调 (Module Coordination):它创建并持有一系列专门的“管理器”(Manager),如
MenuBarManager
、SettingsManager
等,并将它们串联起来协同工作。 - 响应式更新 (Reactive Updates):利用 SwiftUI 的
ObservableObject
和 Combine 框架,当状态发生变化时,能够自动通知并更新用户界面 (UI)。 - 提供统一接口 (Providing Unified Interface):为 app 的其他部分提供清晰、统一的方法来访问数据和执行操作(如打开/关闭窗口、激活 app 等)。
代码逐段详解
类的定义和属性
@MainActor
final class AppState: ObservableObject {
// ... 属性列表 ...
}
@MainActor
: 和AppDelegate
一样,确保所有操作都在主 UI 线程上进行,保证界面安全。ObservableObject
: 这是 SwiftUI 框架中的一个核心概念,意味着这个类的对象是**“可被观察的”。你可以把它想象成一个“内容发布频道”**。当它内部被@Published
标记的属性发生变化时,所有“订阅”了这个频道的 SwiftUI 视图都会自动收到通知并更新自己的外观。
属性详解 (Properties)
AppState
的属性可以分为几类:
1. 各个功能的“管理器” (Managers)
private(set) lazy var appearanceManager = MenuBarAppearanceManager(appState: self)
private(set) lazy var eventManager = EventManager(appState: self)
// ... 其他 managers
这是这个 app 架构的核心。作者没有把所有功能都塞进 AppState
这一个类里,而是采用了**“关注点分离” (Separation of Concerns)** 的设计原则,将不同职责委托给专门的管理器类。
private(set)
: 表示这些管理器只能在AppState
内部被创建,但外部可以读取它们。lazy var
: 懒加载。这意味着管理器对象并不会在AppState
初始化时立即创建,而是在第一次被访问时才会被创建。这有助于提高 app 的启动速度并节省初始内存。MenuBarManager
: 管理菜单栏本身的状态(显示/隐藏逻辑)。MenuBarItemManager
: 管理菜单栏中的所有图标项。SettingsManager
: 管理应用的各项设置。PermissionsManager
: 管理和检查系统权限。UpdatesManager
: 负责检查和处理应用更新。- …等等。每个管理器都专注于自己的一块领域。
2. 核心状态属性 (Core State Properties)
@Published private(set) var isActiveSpaceFullscreen: Bool
weak var appDelegate: AppDelegate?
weak var settingsWindow: NSWindow?
private var cancellables = Set<AnyCancellable>()
@Published
: 这是ObservableObject
的“魔法棒”。任何被它标记的属性,一旦其值发生改变,就会自动“发布”通知,触发 UI 更新。这里的isActiveSpaceFullscreen
就记录了当前桌面空间是否处于全屏模式。weak var ...?
: 与AppDelegate
中一样,这里使用弱引用来持有对appDelegate
和各个窗口 (settingsWindow
,permissionsWindow
) 的引用,以防止循环引用导致的内存泄漏。cancellables
: 这是使用苹果的 Combine 框架进行响应式编程的关键。它像一个文件夹,用来存放所有创建的“订阅”(observers)。当AppState
对象被销毁时,这个集合里的所有订阅都会被自动取消,防止了不必要的后台活动和内存泄漏。
3. 实用工具属性 (Utility Properties)
let isPreview: Bool = { ... }()
var setsCursorInBackground: Bool { ... }
isPreview
: 一个很聪明的属性,它通过检查环境变量来判断当前代码是否正运行在 Xcode 的 SwiftUI 预览模式下。这允许开发者在预览时提供模拟数据或禁用某些功能。setsCursorInBackground
: 这个属性通过调用底层的Bridging
代码来设置一个系统连接属性,决定了 app 是否能在后台更改鼠标指针。
核心方法详解 (Core Methods)
设置与配置
performSetup()
这是 app 的主安装程序。在 AppDelegate
中,当权限检查通过后,就会调用这个方法。它的逻辑非常清晰:依次调用它所持有的每一个管理器的 performSetup()
方法,像一个总指挥官命令各个部门开始工作。
assign...()
方法
assignAppDelegate(_:)
, assignSettingsWindow(_:)
等方法,是用来让外部组件(如 AppDelegate
或 SwiftUI 视图)将它们自己或它们创建的窗口实例传递给 AppState
。AppState
从而获得了对这些关键对象的引用。
configureCancellables()
这是整个 AppState
中最复杂也最核心的方法之一,是响应式编程的集中体现。它的主要作用是设置一系列的“监听器”或“订阅”,来响应各种系统事件,并自动更新 AppState
的状态。
让我们看几个例子来理解它的工作模式:
-
监听全屏状态变化:
Publishers.Merge3( ... ) .sink { [weak self] _ in self?.isActiveSpaceFullscreen = ... } .store(in: &c)
这里它合并 (
Merge3
) 了三个不同的事件源:1) 工作区切换通知, 2) 最前方的应用变化, 3) 鼠标左键点击。这三个事件中任何一个发生,都可能意味着用户切换到了全屏应用。sink
部分就是**“订阅者”**,当接收到合并后的事件时,它就会执行代码,重新检查并更新isActiveSpaceFullscreen
属性。最后,store(in: &c)
将这个“订阅”存入cancellables
文件夹,让它保持激活状态。 -
监听子管理器的变化:
menuBarManager.objectWillChange .sink { [weak self] in self?.objectWillChange.send() } .store(in: &c)
这是一种**“变化传递”**机制。
AppState
不仅自己发布变化,它还“订阅”了它的子管理器(如menuBarManager
)的变化。当menuBarManager
即将发生变化时,它会通知AppState
,然后AppState
再通过objectWillChange.send()
告诉所有观察它的 SwiftUI 视图:“嘿,我(以及我的一个子组件)要变了,你们准备更新吧!” 这确保了整个 app 的状态同步。
窗口与激活管理
open...Window()
/dismiss...Window()
: 提供了简单的命令来打开和关闭设置及权限窗口。activate(withPolicy:)
/deactivate(withPolicy:)
: 这两个方法封装了复杂的应用激活逻辑。activate
负责将应用带到最前台。它甚至包含了一个有趣的“黑科技” (hack):在首次激活时,它会先激活一下系统的 Dock,再激活自己,以确保 app 图标能正确地从 Dock 中“弹跳”出来并获得焦点。deactivate
则负责将应用的控制权交还给上一个应用,并根据需要改变其激活策略(比如,在关闭窗口后变为一个没有 Dock 图标的.accessory
应用)。
总结
AppState.swift
是一个精心设计的中心枢纽。它通过懒加载的管理器来分离职责,保持自身代码的整洁;同时利用 Combine 和 @Published
实现了强大的响应式数据流,让 UI 能够自动、高效地对状态变化做出反应。它是连接底层逻辑、系统事件和顶层 SwiftUI 视图的关键桥梁。