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