You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI macOS应用:阻止「打开最近」菜单因主窗口UI频繁更新而自动关闭

SwiftUI macOS应用:阻止「打开最近」菜单因主窗口UI频繁更新而自动关闭

我完全懂你的困扰——做音频播放器嘛,主UI肯定得跟着播放状态每秒更新,但没想到这居然会连累「打开最近」菜单,刚点开就自动关闭,体验太糟了。咱们来一步步解决这个问题,核心思路就是把菜单的更新逻辑和主UI的频繁刷新彻底隔离开。

问题根源拆解

你的主窗口UI之所以会频繁更新,是因为AudioDiscPlayer里的一堆@Published属性在实时推送状态变化。而SwiftUI的App结构体里的body(包括你定义的commands)会跟着所有绑定的ObservableObject一起刷新——也就是说,主UI每更一次,整个命令菜单都会被重建一遍,这就导致刚打开的「打开最近」菜单直接被刷没了。

解决方案:隔离菜单的更新逻辑

咱们要做的就是让「打开最近」菜单只在最近文件列表真的变化时才更新,完全不受主UI刷新的影响。具体分三步:

1. 优化RecentDiscsModel:用系统通知自动更新,摆脱手动调用

原来你是手动调用refresh(),现在咱们监听系统自带的通知,让最近文件列表自动更新,还能避免不必要的刷新:

class RecentDiscsModel: ObservableObject {
    static let shared = RecentDiscsModel()
    @Published private(set) var recentDocs: [URL] = []
    
    private init() {
        // 初始化时先加载一次
        refresh()
        // 监听系统的最近文档变化通知,自动触发更新
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(refresh),
            name: NSDocumentController.didChangeRecentDocumentsNotification,
            object: NSDocumentController.shared
        )
    }
    
    @objc func refresh() {
        let newDocs = NSDocumentController.shared.recentDocumentURLs
        // 只有当列表真的变化时才更新,避免无意义的刷新
        if newDocs != recentDocs {
            recentDocs = newDocs
        }
    }
}

2. 重构App的Commands:让菜单只依赖RecentDiscsModel

原来你直接在菜单里用NSDocumentController.shared.recentDocumentURLs,这会导致每次App body刷新都重建菜单。现在改成绑定RecentDiscsModel的属性,只有当最近文件变化时才重建菜单:

@main
struct MyApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @StateObject private var recentDiscsModel = RecentDiscsModel.shared
    
    var body: some Scene {
        Window("Player", id: "main-window") {
            PlayerView()
        }
        .commands {
            CommandGroup(replacing: .newItem) { }
            CommandGroup(after: .newItem) {
                Button("Open...") {
                    NotificationCenter.default.post(name: .openDocumentRequested, object: nil)
                }
                .keyboardShortcut("O", modifiers: .command)
                
                // 只在recentDiscsModel的列表非空时显示菜单
                if !recentDiscsModel.recentDocs.isEmpty {
                    Menu("Open Recent") {
                        // 遍历model里的列表,而不是直接用NSDocumentController的属性
                        ForEach(recentDiscsModel.recentDocs, id: \.self) { url in
                            Button(url.lastPathComponent) {
                                Globals.shared.insertDisc(at: url)
                            }
                        }
                        Divider()
                        Button("Clear Menu") {
                            NSDocumentController.shared.clearRecentDocuments(nil)
                            // 现在不需要手动调用refresh,系统通知会自动处理
                        }
                    }
                }
            }
            PlayBackMenu()
        }
    }
}

3. 简化Globals的insertDisc方法:去掉手动刷新

现在有了系统通知的自动更新,就不用再手动调用RecentDiscsModel.shared.refresh()了:

class Globals: ObservableObject {
    static let shared = Globals()
    init() {}
    
    @Published var errorMessage = ""
    @Published var errorDetails = ""
    @Published var showingErrorAlert = false
    
    func handleFileImport(result: Result<URL, Error>) {
        switch result {
        case .success(let url):
            guard url.startAccessingSecurityScopedResource() else {
                errorMessage = "Unable to access file"
                errorDetails = "Permission denied"
                showingErrorAlert = true
                return
            }
            defer { url.stopAccessingSecurityScopedResource() }
            insertDisc(at: url)
        case .failure(let error):
            errorMessage = "Failed to import file"
            errorDetails = error.localizedDescription
            showingErrorAlert = true
        }
    }
    
    func insertDisc(at url: URL, startPlayback: Bool = false) {
        do {
            try AudioDiscPlayer.shared.insertDisc(at: url, startPlayback: startPlayback)
            NSDocumentController.shared.noteNewRecentDocumentURL(url)
            // 移除手动调用refresh的代码
        } catch {
            let nsError = error as NSError
            errorMessage = nsError.localizedDescription
            let reason = nsError.localizedFailureReason ?? ""
            let recovery = nsError.localizedRecoverySuggestion ?? ""
            errorDetails = "\n\n\(reason)\n\n\(recovery)".trimmingCharacters(in: .whitespacesAndNewlines)
            showingErrorAlert = true
        }
    }
}

为什么这样能解决问题?

  • 隔离刷新源:现在「打开最近」菜单只绑定RecentDiscsModel,只有当最近文件列表真的变化时,菜单才会重建,完全不受主UI每秒更新的影响。
  • 利用系统通知:借助NSDocumentController的原生通知,自动处理最近文件的添加/清除,既可靠又不用自己写额外的监听逻辑。
  • 减少无意义刷新:RecentDiscsModel里的refresh()只会在列表真的变化时更新@Published属性,避免了不必要的UI重建。

额外优化建议

如果你的PlayBackMenu也依赖AudioDiscPlayer的实时状态,建议把它的数据源也做类似隔离,比如用@EnvironmentObject在View层级传递,而不是放在App结构体里,进一步减少App body的刷新频率。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:49:30