【问题标题】:Swift macOS popover detect change dark modeSwift macOS 弹出框检测更改暗模式
【发布时间】:2025-12-18 11:05:01
【问题描述】:

如图所示,我有一个弹出框。

我必须确保当屏幕模式发生变化时,暗模式或亮模式,弹出框的颜色会发生变化。

颜色取自资产,如下所示:

NSColor(named: "backgroundTheme")?.withAlphaComponent(1)

从代码中可以看出,在 init 函数中启动弹出框时,我相应地分配了颜色。

如何拦截模式的变化?

你能帮帮我吗?

AppDelegate:

import Cocoa
import SwiftUI

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    var popover = NSPopover.init()
    var statusBar: StatusBarController?
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let contentView = ContentView()
        popover.contentSize = NSSize(width: 560, height: 360)
        popover.contentViewController = NSHostingController(rootView: contentView)
        statusBar = StatusBarController.init(popover)
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
}

状态栏控制器:

import AppKit
import SwiftUI

extension NSPopover {
    
    private struct Keys {
        static var backgroundViewKey = "backgroundKey"
    }
    
    private var backgroundView: NSView {
        let bgView = objc_getAssociatedObject(self, &Keys.backgroundViewKey) as? NSView
        if let view = bgView {
            return view
        }
        
        let view = NSView()
        objc_setAssociatedObject(self, &Keys.backgroundViewKey, view, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        NotificationCenter.default.addObserver(self, selector: #selector(popoverWillOpen(_:)), name: NSPopover.willShowNotification, object: nil)
        return view
    }
    
    @objc private func popoverWillOpen(_ notification: Notification) {
        if backgroundView.superview == nil {
            if let contentView = contentViewController?.view, let frameView = contentView.superview {
                frameView.wantsLayer = true
                backgroundView.frame = NSInsetRect(frameView.frame, 1, 1)
                backgroundView.autoresizingMask = [.width, .height]
                frameView.addSubview(backgroundView, positioned: .below, relativeTo: contentView)
            }
        }
    }
    
    var backgroundColor: NSColor? {
        get {
            if let bgColor = backgroundView.layer?.backgroundColor {
                return NSColor(cgColor: bgColor)
            }
            return nil
        }
        set {
            backgroundView.wantsLayer = true
            backgroundView.layer?.backgroundColor = newValue?.cgColor
        }
    }
}

class StatusBarController {
    private var popover: NSPopover
    private var statusBar: NSStatusBar
    var statusItem: NSStatusItem

    
    init(_ popover: NSPopover) {
        self.popover = popover
        self.popover.backgroundColor = NSColor(named: "backgroundTheme")?.withAlphaComponent(1)
        statusBar = NSStatusBar.init()
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        
        if let statusBarButton = statusItem.button {
            statusBarButton.image = #imageLiteral(resourceName: "Fork")
            statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0)
            statusBarButton.image?.isTemplate = true
            statusBarButton.action = #selector(togglePopover(sender:))
            statusBarButton.target = self
            statusBarButton.imagePosition = NSControl.ImagePosition.imageLeft
        }
    }
    
    @objc func togglePopover(sender: AnyObject) {
        if(popover.isShown) {
            hidePopover(sender)
        }else {
            showPopover(sender)
        }
    }
    
    func showPopover(_ sender: AnyObject) {
        if let statusBarButton = statusItem.button {
            popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
        }
    }
    
    func hidePopover(_ sender: AnyObject) {
        popover.performClose(sender)
    }
    
}

【问题讨论】:

  • @ElTomato:它似乎有效,但在 interfaceModeChanged 内,当我更改时我无法恢复背景主题颜色。 popover.backgroundColor = NSColor(named:" backgroundTheme")?.withAlphaComponent(1)
  • 在 macOS 中,观察外观变化需要两个步骤。您可以在视图 (SwiftUI) 或视图控制器 (Cocoa) 中查看当前外观(浅绿色或深色)。这还不够。您还需要让应用程序通过AppDelegate 观察外观变化,以便它知道用户何时使用系统偏好进行更改。
  • @ElTomato:那我该如何解决这个问题呢?
  • "backgroundView.layer?.backgroundColor = newValue?.cgColor" -> 你不应该在颜色设置器中设置图层颜色。使用 setNeedsUpdatelayer 并在更新图层中进行图层颜色分配。我建议观看 WWDC18 Advanced Dark Mode,其中提到了为什么你的代码不起作用;developer.apple.com/videos/play/wwdc2018/218

标签: swift macos popover appkit nspopover


【解决方案1】:

您好,我会跳过在弹出框上设置颜色,而是在 ContentView.swift 中设置背景

然后将背景设置为包装 UI 其余部分的 VStack/HStack/ZStack。

var body: some View {
        VStack{
            Text("Hello, world!").padding()
            Button("Ok", action: {}).padding()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color("backgroundTheme").opacity(0.3))
        .padding(.top, -16)
    }

【讨论】:

  • 我已经这样做了,但它不起作用,因为箭头会有不同的颜色,如果你使用透明度会更糟。
  • 啊,在此之前我读到了你的另一个问题。您在哪里询问如何关闭弹出窗口中的箭头所以以为您不再拥有它了。
  • @Paul 我刚刚编辑了我上面的代码,这应该可以帮助你。小技巧......但是,嘿,它有效;)
  • @Paul 如果解决了您的问题,请考虑接受并支持我的回答。 :)
【解决方案2】:

有一些事情要记住:

  • .withAlphaComponent(_:) 这样将现有NSColor 转换为新颜色的方法不会返回动态颜色。

  • CGColor 不支持动态。使用.cgColorNSColor 转换为CGColor 时,您正在从NSColor 的“当前”颜色转换。

所以你的hacky方式并不是你想要的好方法。


根据您所说的,如果我理解正确,您想在弹出框的背景中添加颜色叠加层,包括箭头部分。

您实际上可以在视图控制器中完成所有这些操作:

class PopoverViewController: NSViewController {
    /// for color overlay
    lazy var backgroundView: NSBox = {
        // 1. This extend the frame to cover arrow potion.
        let box = NSBox(frame: view.bounds.insetBy(dx: -13, dy: -13))
        box.autoresizingMask = [.width, .height]
        box.boxType = .custom
        box.titlePosition = .noTitle
        box.fillColor = NSColor(named: "backgroundTheme")
        return box
    }()
    
    /// for mounting SwiftUI views
    lazy var contentView: NSView = {
        let view = NSView(frame: view.bounds)
        view.autoresizingMask = [.width, .height]
        return view
    }()
    
    override func loadView() {
        view = NSView()
        
        // 2. This avoid clipping.
        view.wantsLayer = true
        view.layer?.masksToBounds = false

        view.addSubview(backgroundView)
        view.addSubview(contentView)
    }
}

注意12,这允许backgroundView 绘制超出view 的边界,覆盖箭头部分。 backgroundView 是一个 NSBox 对象,它接受一个动态的 NSColor 对象来设置其背景样式。

请注意,如果您想更改颜色的不透明度,而不是 .withAlphaComponent(_:),请更改资源的不透明度,就在 RGB 滑块的正下方。

contentView 在这里作为 SwiftUI 视图的挂载点。要从 NSHostingController 挂载内容,您可以:

let popoverViewController = PopoverViewController()
_ = popoverViewController.view // this trigger `loadView()`, you don't need this for auto layout

let hostingController = NSHostingController(rootView: ContentView())
hostingController.view.frame = popoverViewController.contentView.bounds
hostingController.view.autoresizingMask = [.width, .height]

popoverViewController.contentView.addSubview(hostingController.view)
popoverViewController.addChild(hostingController)

这会将hostingControllerview 添加为popoverViewControllercontentView 的子视图。

就是这样。

请注意,我使用 autoresizingMask 而不是自动布局,并将安装部分从 PopoverViewController 中提取出来,以简单地回答。

【讨论】: