SwiftUI - How to disable sidebar from collapsing?

Viewed 924
3 Answers

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

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
}

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.

Related