肇鑫的技术博客

业精于勤,荒于嬉

macOS系统菜单栏显示多图标的两种方式

一直以来都是使用的菜单栏单图标的方式。今天心血来潮,想在Focus原本的图标旁边新增一个刷新的图标,可以用来快速重置计时器。

把任务分配给AI,AI很快完成了第一版。我运行一看,怎么没看到新图标,再仔细一找,的确是两个图标了,但是两个图标是各自独立的,彼此之间还间隔了几个其它的图标。这和我看到的不一样,我其实希望的是像音乐播放器那样的,几个图标一体的方式。于是我和AI说明了要一体的。AI表示了解,然后重新生成了代码。AI特意解释说,一体之后,点击时就需要用点击的位置进行二次判断,还确定用户点击的是哪个图标。

最终结果我实验了一下,的确和音乐播放器的一样。看来他们也是这么做的。

SwiftUI应用伴随系统登录自动启动后显示macOS应用的窗口的办法

我们知道SwiftUI应用本身没有应用窗口的概念,它是使用WindowGroup来自动管理窗口的。这在用户手动启动应用的时候没有问题。但是如果你设置了应用伴随系统登录后自动启动后,SwiftUI的应用会存在一些问题。

这是因为这种方式启动应用后,SwiftUI的应用不会主动创建窗口,视图会在用户手动点击应用之后才创建。而这可能不是我们所需要的。因为如果我们有使用onAppear来执行一些代码。我们实际上是希望代码可以在应用启动时就运行,而这个机制会导致执行会推迟到用户点击时。

因此,我们需要保证即便SwiftUI没有生成Window,也要自己主动来生成Window。

问题分析

要解决这个问题,我们首先要了解这个启动的整个过程,然后才能知道如何来改进。具体调试的过程我就不讲了,最终我确认启动的过程是这样的:

  1. 用户登录。
  2. 应用启动。
  3. 应用在后台启动,但是没有主窗口,因此无法自动切换到前台。

我们可以使用NSApplication.shared.windows.count来判断。如果是用户手动打开的应用,SwiftUI会创建SwiftUI.AppKitWindow的窗口。而如果是跟随系统登录后后台启动,则不会有这个窗口。

我使用的判断函数是这个:

func isLaunchedAtLogin() -> Bool {
  NSApplication.shared.windows.count < 2
}

之所以用2,而不是1判断。是因为我还使用了菜单栏图标,菜单栏图标使用的是NSStatusItem,因此还会包含一个叫NSStatusWindow的窗口。

解决方案

func applicationDidFinishLaunching(_ notification: Notification) {
    createContentViewWindow()
}

/// 用户启动时SwiftUI的窗口会先创建,因此window不会为nil,但是有可能存在延迟,因为是通过.updateWindow还获取的。
/// 所以这里使用其他的方式进行判断,而不是使用window是否为nil
private func createContentViewWindow() {
  /// 若是用户点击,会有两个窗口,一个是SwiftUI创建的主窗口,一个status窗口。后者应该是对应菜单栏图标的。
  /// 如果是伴随系统启动,则SwiftUI创建的主窗口不存在,只有status的窗口。
  func isLaunchedAtLogin() -> Bool {
    NSApplication.shared.windows.count < 2
  }

  if isLaunchedAtLogin() {
    let contentView = ContentView()
      .environment(\.managedObjectContext, ModelProvider.shared.container.viewContext)
    let window = NSWindow(
      contentRect: NSRect(x: 0, y: 0, width: 400, height: 240),
      styleMask: [.titled, .closable, .miniaturizable, .resizable],
      backing: .buffered, defer: false)

    window.setFrameAutosaveName("Main Window")
    window.contentView = NSHostingView(rootView: contentView)
    window.center()
    window.makeKeyAndOrderFront(nil)
    self.window = window
  }
}

小结

传统上,如果我们使用loginItem来实现应用伴随系统启动,那么为了区分是用户手动启动,还是伴随系统启动,需要使用传递参数的方式。但是传递参数,就需要使用额外的launcher辅助应用。

设置辅助应用的步骤是很复杂的。因此,我们现在大多都是直接使用下面的代码来直接使用自动伴随系统登录启动。

try SMAppService.mainApp.register()

这个办法虽然大大简化了设置系统启动后启动应用的步骤。但是这么做之后,由于没有辅助应用,也就没法使用传递参数的办法了。

本文给出的使用NSApplication.shared.windows.count,用窗口数量来间接判断的方式,利用了SwiftUI在后台启动后,不会主动创建窗口的特性,解决了这个问题。

解决Xcode Cloud无法enable Swift Package包中的宏的问题

最近我在做应用适配iOS/macOS 26的特性。今天在Xcode Cloud打包的时候遇到打包失败的错误。

Macro “DefaultsMacrosDeclarations” from package “Defaults” must be enabled before it can be used.

这个问题是我应用所使用的第三方的库 “Defaults”在其内部使用了宏。这个宏在Xcode本地编译时,需要用户手动点击确认才能继续。但是Xcode Cloud中,没有点击确认的位置。因此,就无法完成打包应用的过程。

解决办法

通过运行脚本的方式,在克隆完文件夹之后,运行脚本,规避掉对于宏的验证。

#!/bin/sh 
defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES

必须在Xcode中的根部位置,创建一个新组,命名为ci_scripts,然后在这个组中创建ci_post_clone.sh,内容是上面的内容。

必须在Xcode的根部位置创建组,并且命名也不能错。

小插曲

我其实最开始是像GPT 4.1提出了这个问题。GPT 4.1的解答只对了一半。它提出了创建文件夹和脚本,文件夹是正确的,脚本名字是错误的。并且它也没有告诉需要在Xcode中创建组,而只是说在项目的根目录创建就可以。最后,它创建的脚本内容不完全正确。

之后我使用了Google搜索。Google搜索默认的AI总结的是正确的,但应该就是从stackoverflow里的答案总结的。我最后是看的SO里的回答,进行的总结。

另外,我建议你完整阅读下面的第一个引用。我使用了里面最为简便的方案。而非最安全的。也许你看了之后,会选择一条不同的手段。

引用文献

How do I trust a swift macro target for Xcode Cloud builds?

Writing custom build scripts