使用 blendMode 防止带有 ContentView 的透明 NSWindow 闪烁

Prevent transparent NSWindow with a ContentView using blendMode from flickering

下面创建了一个透明的 NSWindow 和一个 ContentView,它使用 blendMode 来创建滤色器叠加效果,这样 window 后面的所有东西看起来都是混合的(灰色在这种情况下是单色的)。它按预期工作,除非 window 未激活或被拖动,在这种情况下 ContentView 在正常(无混合)和混合之间闪烁; ContentView 在某些情况下也显示为脏,即当不活动时 ContentView 正在部分渲染且未完全更新。

我是否遗漏了与 NSWindow 事件相关的 ContentView 生命周期/刷新方面的内容,我的 NSWindow 设置是否正确,或者这是一个潜在的错误?本质上,不使用 blendMode 时不会出现此问题,因为使用透明 NSWindow 和半不透明 ContentView 进行测试时表现正常。

我在 Big Sur 11.6.2 上使用 Xcode 12.5.1,目标是 10.15

使用 AppKit App Delegate 生命周期模板重现的代码:

class AppDelegate: NSObject, NSApplicationDelegate {

    var window: NSWindow!


    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()
            .edgesIgnoringSafeArea(.top)
            .blendMode(BlendMode.color)
        
        // Create the window and set the content view.
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled, .closable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.isReleasedWhenClosed = false
        window.center()
        window.setFrameAutosaveName("Main Window")

        window.isOpaque = false
        window.backgroundColor = .clear
        window.level = .floating
        window.isMovable = true
        window.isMovableByWindowBackground = true
        window.titlebarAppearsTransparent = true
        
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
    }

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

struct ContentView: View {
    var body: some View {
        Rectangle()
            .fill(Color.black)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

3 月 7 日更新

问题仍然存在,在 Monterey 12.2.1 上使用 Xcode 13.2.1,并以 12.2

为目标

3 月 8 日更新

添加默认的 NSVisualEffectView 背景视图会带来更高的稳定性,因为当 window 处于活动状态并被拖动时,视图不再在不透明和透明之间闪烁。

剩下的唯一问题是在应用程序之间切换时失去焦点,这有时会导致视图变得不透明,尽管重新聚焦 window 可以解决问题。

解决方法是在 NSWindow 上启用 hidesOnDeactivate,并结合 applicationShouldHandleReopen,这样 window 会在失去焦点时消失并且问题不可见用户,但理想情况下 window 应始终保持可见状态,直到关闭。

struct VisualEffectView: NSViewRepresentable {
    func makeNSView(context: Context) -> NSVisualEffectView {
        let view = NSVisualEffectView()
        return view
    }

    func updateNSView(_ nsView: NSVisualEffectView, context: Context) {

    }
}


struct ContentView: View {
    var body: some View {
        Rectangle()
            .fill(Color.black)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(VisualEffectView())

    }
}

这感觉有点老套,我敢肯定知识渊博的人有更优雅的解决方案,但是在视图中添加 Timer 以强制重绘完全解决了闪烁问题,并且会因此似乎回答了这个问题。 注意:此方法还省去了对虚拟对象的需要 NSVisualEffectView

struct ContentView: View {
    @State var currentDate = Date()

    let timer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect()

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.black)
                .frame(maxWidth: .infinity, maxHeight: .infinity)

            Text("\(currentDate)")
                .foregroundColor(.clear)
                .onReceive(timer) { input in
                    currentDate = input
                }
        }
    }
}