2025-07-14 09:34:07

借助AI学习开源项目-AppState

继续解读下面的相关代码:

// // 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 的类,它的主要职责是:

  1. 状态管理 (State Management):作为**“唯一事实来源 (Single Source of Truth)”**,集中存储整个 app 的状态,比如当前是否全屏、设置项的值等。
  2. 模块协调 (Module Coordination):它创建并持有一系列专门的“管理器”(Manager),如 MenuBarManagerSettingsManager 等,并将它们串联起来协同工作。
  3. 响应式更新 (Reactive Updates):利用 SwiftUI 的 ObservableObject 和 Combine 框架,当状态发生变化时,能够自动通知并更新用户界面 (UI)。
  4. 提供统一接口 (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 视图)将它们自己或它们创建的窗口实例传递给 AppStateAppState 从而获得了对这些关键对象的引用。

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 视图的关键桥梁。

本文链接:http://blog.go2live.cn/post/ice-AppState.html

-- EOF --