swiftmacosswiftuinswindownsvisualeffectview

My NSWindow's shadow is getting cut off when switching screens?


I wanted to have an NSWindow with a blurred background so I created a wrapper for NSVisualEffectView to be used in my ContentView() with some help from How do you blur the background in a SwiftUI macOS application?. I also tried doing it with just the NSWindow using https://github.com/lukakerr/NSWindowStyles#:~:text=true-,6.%20Vibrant%20background,A,-vibrant.

struct VisualEffectView: NSViewRepresentable
{
    let material: NSVisualEffectView.Material
    let blendingMode: NSVisualEffectView.BlendingMode
    
    func makeNSView(context: Context) -> NSVisualEffectView
    {
        let visualEffectView = NSVisualEffectView()
        visualEffectView.material = material
        visualEffectView.blendingMode = blendingMode
        visualEffectView.state = NSVisualEffectView.State.active
        return visualEffectView
    }

    func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context)
    {
        visualEffectView.material = material
        visualEffectView.blendingMode = blendingMode
    }
}

It works and looks great, however, when I move the window to a different screen -and pause with the window between both screens, then move it to the next window- it chops off a part of the NSWindow's shadow.

This is what it looks like when moving screens ⤵︎

enter image description here

Is there a way to prevent this shadow chop from happening?

Interface: SwiftUI
LifeCycle: Appkit AppDelegate


Solution

  • Figured it out! Without any hacks too thankfully lol

    enter image description here

    Rules

    In order to achieve this look without the nasty artifacts in the question you have to do a few things the way macOS wants them.

    1. Don't set your NSWindow.backgroundColor = .clear!
    This is the cause for the nasty artifacts above in the first place! Leaving your window's color as is will make sure the window functions properly when changing screens. NSVisualEffectView captures the image behind the window and uses that for the background so there's no need to make anything transparent.

    2. Make sure to include .titled in the window's styleMask!
    Failure to do so will render the window without rounded corners. If you attempt to add rounded corners (like I did) to the SwiftUI view you will still have an opaque background on the NSWindow itself. If you then set your window's background color to .clear (like I did again) the shadow chop issues will ensue! However, this does not mean that the title bar will get in the way, it won't, we'll get to that in a bit.

    3. Add your NSVisualEffectView to your SwiftUI view!
    I found this to be easier than adding the visual effect to the NSWindow.contentView as a subview.

    Solution

    1. So start off by setting up your NSWindow and AppDelegate! ⤵︎
    All you're doing is making sure the titlebar is present but hidden.

    import Cocoa
    import SwiftUI
    
    @main
    class AppDelegate: NSObject, NSApplicationDelegate {
    
        var window: NSWindow!
    
        func applicationDidFinishLaunching(_ aNotification: Notification) {
    
            // Create the SwiftUI view that provides the window contents.
            let contentView = ContentView()
    
            // Create the window and set the content view.
            // Note: You can add any styleMasks you want, just don't remove the ones below.
            window = NSWindow(
                contentRect: NSRect(x: 0, y: 0, width: 300, height: 200),
                styleMask: [.titled, .fullSizeContentView],
                backing: .buffered, defer: false)
    
            // Hide the titlebar
            window.titlebarAppearsTransparent = true
            window.titleVisibility = .hidden
    
            // Hide all Titlebar Controls
            window.standardWindowButton(.miniaturizeButton)?.isHidden = true
            window.standardWindowButton(.closeButton)?.isHidden = true
            window.standardWindowButton(.zoomButton)?.isHidden = true
    
            // Set the contentView to the SwiftUI ContentView()
            window.contentView = NSHostingView(rootView: contentView)
    
            // Make sure the window is movable when grabbing it anywhere
            window.isMovableByWindowBackground = true
    
            // Saves frame position between opening / closing
            window.setFrameAutosaveName("Main Window")
    
            // Display the window
            window.makeKeyAndOrderFront(nil)
            window.center()
        }
    
        func applicationWillTerminate(_ aNotification: Notification) {
            // Insert code here to tear down your application
        }
    }
    

    Your window will probably look something like this at this point (if starting with a blank project). You can see the 'Hello world!' isn't exactly centred due to the title bar. ⤵︎

    enter image description here


    2. Once your NSWindow is setup, time to do the ContentView() ⤵︎
    In here you just want to create a wrapper for NSVisualEffectView and add it as a background. AND THEN make sure you remove the safe areas from the view! This makes sure to get rid of any space the title bar was eating up in the view.

    import SwiftUI
    
    struct ContentView: View {
        var body: some View {
            Text("Hello, World!")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(VisualEffectView(material: .popover, blendingMode: .behindWindow))
    
                // Very important! (You could technically just ignore the top so you do you)
                .edgesIgnoringSafeArea(.all)
        }
    }
    
    /// Takes the image directly behind the window and uses that to create a blurred material. It can technically be added anywhere but most often it's used as a backing material for sidebars and full windows.
    struct VisualEffectView: NSViewRepresentable {
        let material: NSVisualEffectView.Material
        let blendingMode: NSVisualEffectView.BlendingMode
    
        func makeNSView(context: Context) -> NSVisualEffectView {
            let visualEffectView = NSVisualEffectView()
            visualEffectView.material = material
            visualEffectView.blendingMode = blendingMode
            visualEffectView.state = NSVisualEffectView.State.active
            return visualEffectView
        }
    
        func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) {
            visualEffectView.material = material
            visualEffectView.blendingMode = blendingMode
        }
    }
    

    At this point your view should look how you want it without any negative effects! Enjoy <3 (If you're having any problems with this solution leave a comment!)

    enter image description here

    Resources

    Thank you to @eonil for this clever way to keep the rounded corners on. Couldn't have figured this out without this answer ⤵︎
    https://stackoverflow.com/a/27613308/13142325

    Thank you to lukakerr for making this list of NSWindow styles! https://github.com/lukakerr/NSWindowStyles