xcodeswiftuinsviewnswindownsvisualeffectview

Prevent transparent NSWindow with a ContentView using blendMode from flickering


The following creates a transparent NSWindow with a ContentView that uses blendMode to create a colour filter overlay effect so that everything behind the window appears blended (grey monochrome in this instance). It's working as expected except when the window is not active or being dragged in which case the ContentView flickers between normal (no blending) and blended; the ContentView is also showing dirty in some cases, i.e. when inactive ContentView is partially rendering and not fully updated.

Am I missing something in terms of ContentView life-cycle / refresh in relation to NSWindow events, is my NSWindow setup correct, or is this a potential bug? Essentially, the issue doesn't occur when blendMode isn't used, as testing with a transparent NSWindow and semi-opaque ContentView behaves normally.

I'm using Xcode 12.5.1 on Big Sur 11.6.2, and targeting 10.15

Code to reproduce using the AppKit App Delegate lifecycle template:

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)
    }
}

UPDATE 7th March

Issue persists using Xcode 13.2.1 on Monterey 12.2.1, and targeting 12.2

UPDATE 8th March

Adding a default NSVisualEffectView background view results in much greater stability in that the view no longer flickers between opaque and transparent when the window is active and being dragged.

The only issue remaining is when switching between apps and the focus is lost which sometimes causes the view to become opaque, although refocusing the window fixes the problem.

A workaround is to enable hidesOnDeactivate on NSWindow, combined with applicationShouldHandleReopen, so the window disappears when focus is lost and the issue isn't visible to the user, but ideally the window should remain visible at all times until closed.

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())

    }
}

Solution

  • This feels a bit hackish and I'm sure someone out there with greater knowledge has a more elegant solution, but adding a Timer to the view to force a redraw solves the flickering problem completely, and would therefore appear to answer the question. Note: this method also dispenses with the need for a dummy 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
                    }
            }
        }
    }