Search code examples
swiftuiswiftui-navigationsplitview

NavigationSplitView toolbar in fullscreen does not display background image


MacOS application built using SwiftUI. I am replicating a transparent background for NavigationSplitView by setting a background image for the detail and sidebar views.

//NavigationSplitView
NavigationSplitView {
    VStack {
        //Sidebar views
    }.background(
        SidebarBackgroundView()
    )
    .navigationSplitViewColumnWidth(ideal: 200)

} detail: {
    //Detail Views
}
.toolbar {
    ToolbarItem(placement: ToolbarItemPlacement.navigation) {
        StatusManagerView()
    }
}
.ignoresSafeArea()
.background(
    SidebarBackgroundView()
)

//Background image / view
struct SidebarBackgroundView : View {
    var body : some View {
        GeometryReader { geo in
            Image(backgroundImageName).resizable() // Make the image resizable
                .aspectRatio(contentMode: .fill)
                .blur(radius: 4.0, opaque: true)
                .edgesIgnoringSafeArea(.all)
                .frame(width: geo.size.width, height: geo.size.height, alignment: .topLeading)
            
        }
    }
}
    

IMAGE

This works fine, but I am running into an issue where when i go into fullscreen mode, the image no longer appears under the toolbar.

IMAGE

At this point, Im at a loss of how to approach. Ive looked to see if i can reforce the background to be redrawn, looked if there are properties ive missed, explicitly set background on toolbar.

Anyone have any suggestions or ideas of what might be going on here?


Solution

  • The main issue here is that NSToolbar adds a blur view as its background when the app goes into fullscreen. Your background is still technically under the toolbar.

    So the ideal solution would be to completely remove the background of the toolbar, however it doesn't seem possible to remove it when the app is in fullscreen mode.

    One solution could be to auto hide the toolbar when the app goes into fullscreen mode. The toolbar appears when the cursor is moved to the top edge of the window and hides when it moves away.

    To achieve this in SwiftUI, you have to do the following:

    • create a custom window delegate which implements willUseFullScreenPresentationOptions to auto hide the toolbar on fullscreen
    • find the underlying window of the view and set its delegate to your custom delegate

    I have posted an answer here to achieve this.


    I also noticed that your sidebar and detail backgrounds don't align properly. Here's a quick fix with the full code:

    struct ContentView: View {
        private var customWindowDelegate = CustomWindowDelegate()
        
        var body: some View {
            GeometryReader { geo in
                NavigationSplitView {
                    ZStack {
                        Color.clear // to expand the sidebar to full size
                        Text("Sidebar")
                    }
                    .background(alignment: .topLeading) {
                        self.backgroundImage
                            .frame(width: geo.size.width, height: geo.size.height)
                    }
                } detail: {
                    Text("Detail View")
                }
                .toolbar {
                    ToolbarItem(placement: ToolbarItemPlacement.navigation) {
                        Text("This is the toolbar")
                    }
                }
                .background(alignment: .topLeading) {
                    self.backgroundImage
                        .frame(width: geo.size.width, height: geo.size.height)
                }
            }
            .background {
                HostingWindowFinder { window in
                    guard let window else { return }
                    window.delegate = customWindowDelegate
                    window.titlebarAppearsTransparent = true
                }
            }
        }
        
        var backgroundImage: some View {
            Image("background")
                .resizable()
                .scaledToFill()
                .ignoresSafeArea()
        }
    }
    
    class CustomWindowDelegate: NSObject, NSWindowDelegate {
        override init() {
            super.init()
        }
        
        func window(_ window: NSWindow, willUseFullScreenPresentationOptions proposedOptions: NSApplication.PresentationOptions = []) -> NSApplication.PresentationOptions {
            return [.autoHideToolbar, .autoHideMenuBar, .fullScreen]
        }
    }
    
    struct HostingWindowFinder: NSViewRepresentable {
        var callback: (NSWindow?) -> ()
        
        func makeNSView(context: Self.Context) -> NSView {
            let view = NSView()
            DispatchQueue.main.async { self.callback(view.window) }
            return view
        }
        
        func updateNSView(_ nsView: NSView, context: Context) {
            DispatchQueue.main.async { self.callback(nsView.window) }
        }
    }
    

    GeometryReader gives us the width and height of the window, and the .background(alignment: .topLeading) makes sure both backgrounds are aligned to the top left.