'SwiftUI - How to disable sidebar from collapsing?

Gif to understand easier

Is there any way to disable collapsibility of SidebarListStyle NavigationViews?



Solution 1:[1]

Using this SwiftUI Introspection library: https://github.com/siteline/SwiftUI-Introspect

We can introspect the underlying NSSplitView by extending their functionality:

public func introspectSplitView(customize: @escaping (NSSplitView) -> ()) -> some View {
    return introspect(selector: TargetViewSelector.ancestorOrSibling, customize: customize)
}

And then create a generic extension on View:

public extension View {
    func preventSidebarCollapse() -> some View {
        return introspectSplitView { splitView in
            (splitView.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
        }
    }
}

Which can be used on our sidebar:

var body: some View {
    (...)
    MySidebar()
        .preventSidebarCollapse()
}

Solution 2:[2]

The introspection library mentioned by Oskar is not working for MacOS.

Inspired by that, I figured out a solution for MacOS.

The rationality behind the solution is to use a subtle way to find out the parent view of a NavigationView which is a NSSplitViewController in the current window.

Below codes was tested on XCode 13.2 and macOS 12.1.

var body: some View {
    Text("Replace with your sidebar view")
       .onAppear {
           guard let nsSplitView = findNSSplitVIew(view: NSApp.windows.first?.contentView), let controller = nsSplitView.delegate as? NSSplitViewController else {
                    return
           }
           controller.splitViewItems.first?.canCollapse = false
                // set the width of your side bar here.
           controller.splitViewItems.first?.minimumThickness = 150
           controller.splitViewItems.first?.maximumThickness = 150
       }
}

private func findNSSplitVIew(view: NSView?) -> NSSplitView? {
        var queue = [NSView]()
        if let root = view {
            queue.append(root)
        }
        while !queue.isEmpty {
            let current = queue.removeFirst()
            if current is NSSplitView {
                return current as? NSSplitView
            }
            for subview in current.subviews {
                queue.append(subview)
            }
        }
        return nil
}

Solution 3:[3]

While the method that Oskar used with the Introspect library no longer works, I did find another way of preventing the sidebar from collapsing using Introspect. First, you need to make an extension on View:

extension View {
    public func introspectSplitView(customize: @escaping (NSSplitView) -> ()) -> some View {
        return inject(AppKitIntrospectionView(
            selector: { introspectionView in
                guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
                    return nil
                }
                return Introspect.findAncestorOrAncestorChild(ofType: NSSplitView.self, from: viewHost)
            },
            customize: customize
        ))
    }
}

Then do the following:

NavigationView {
     SidebarView()
         .introspectSplitView { controller in
                    (controller.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
                    
         }
            
     Text("Main View")
                
}

This being said, we don't know how long this will actually work for. Apple could change how NavigationView works and this method may stop working in the future.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Oskar
Solution 2
Solution 3 Jonathan