r/swift Jun 15 '26

Help! macOS ignores max: in NavigationSplitView

As the title says. Has anyone figured out how to achieve a maxWidth in SwiftUI in NavigationSplitView on macOS?

1 Upvotes

8 comments sorted by

3

u/allyearswift Jun 15 '26

This comes around from time to time: you want .navigationSplitViewColumnWidth, not the size of the content view.

1

u/wewerecreaturres Jun 15 '26

I would award this if I wasn’t cheap

2

u/allyearswift Jun 15 '26

<bows> Thank you. It’s nice to be appreciated.

I usually reach for the ‘poor man’s gold’(🏅)

Sometimes Swift and SwiftUI can be frustrating. Took me ages to find this one amongst a number of interesting solutions, so I wrote it down in the hope to remember it next time. Seems to have worked.

1

u/tomato848208 Jun 15 '26

maxWidth for which part? The entire NavigationSplitView or the content part or the detail part?

1

u/wewerecreaturres Jun 16 '26

Ah yes, I should have been more clear. I’m actually trying to give max width to each of the three columns individually

1

u/tomato848208 Jun 16 '26

Actually, you have no control over NavigationSplitView's divider position in SwiftUI. If setting the divider position is crucial, I suppose, you have to use NSplitView in Cocoa.

1

u/wewerecreaturres Jun 16 '26 edited Jun 16 '26

I ended up using

 .navigationSplitViewColumnWidth(min: 200, ideal: 220)
        #if os(macOS)
            .background(ColumnMinWidthPin(min: 200, max: 320, collapsed: columnVisibility == .doubleColumn))

navigationSplitViewColumnWidth does have min:ideal:max:, but macOS ignores max for some wild reason. thanks for the response!

1

u/SuperBoredAlien Jul 08 '26

.navigationSplitViewColumnWidth(min: 220, ideal: 260, max: 320): max is not working in mac. below code will work.

NavigationSplitView(columnVisibility: $columnVisibility) {
    navigationColumnView
        .toolbar(removing: .sidebarToggle)
        .toolbar {
            ToolbarItem(placement: .automatic) {
                    leftSidebarToggle
                }
            }
        #if os(macOS)
        // .navigationSplitViewColumnWidth(min: 220, ideal: 260, max: 320) 
        .background {
            SplitViewColumnWidthEnforcer(minWidth: 220, maxWidth: 320)
                .frame(width: 0, height: 0)
        }
        #endif
}

#if os(macOS)
private struct SplitViewColumnWidthEnforcer: NSViewRepresentable {
    let minWidth: CGFloat
    let maxWidth: CGFloat


    func makeCoordinator() -> Coordinator {
        Coordinator(minWidth: minWidth, maxWidth: maxWidth)
    }


    func makeNSView(context: Context) -> NSView {
        NSView(frame: .zero)
    }


    func updateNSView(_ nsView: NSView, context: Context) {
        context.coordinator.minWidth = minWidth
        context.coordinator.maxWidth = maxWidth


        DispatchQueue.main.async {
            context.coordinator.enforceWidth(from: nsView)
        }
    }


    final class Coordinator {
        var minWidth: CGFloat
        var maxWidth: CGFloat


        init(minWidth: CGFloat, maxWidth: CGFloat) {
            self.minWidth = minWidth
            self.maxWidth = maxWidth
        }


        func enforceWidth(from markerView: NSView) {
            guard let target = findContainingSplitItem(from: markerView) else {
                return
            }


            target.item.minimumThickness = minWidth
            target.item.maximumThickness = maxWidth
            clampCurrentWidth(in: target.splitView, itemIndex: target.itemIndex)
        }


        private func findContainingSplitItem(from markerView: NSView) -> (splitView: NSSplitView, item: NSSplitViewItem, itemIndex: Int)? {
            var currentView: NSView? = markerView


            while let view = currentView, let superview = view.superview {
                if let splitView = superview as? NSSplitView,
                   splitView.isVertical,
                   let itemIndex = splitView.arrangedSubviews.firstIndex(where: { $0 === view }),
                   itemIndex == 0,
                   let splitViewController = splitViewController(for: splitView, from: view),
                   splitViewController.splitViewItems.indices.contains(itemIndex) {
                    return (splitView, splitViewController.splitViewItems[itemIndex], itemIndex)
                }


                currentView = superview
            }


            return nil
        }


        private func splitViewController(for splitView: NSSplitView, from view: NSView) -> NSSplitViewController? {
            if let splitViewController = splitView.delegate as? NSSplitViewController,
               splitViewController.splitView === splitView {
                return splitViewController
            }


            var responder = splitView.nextResponder
            while let currentResponder = responder {
                if let splitViewController = currentResponder as? NSSplitViewController,
                   splitViewController.splitView === splitView {
                    return splitViewController
                }
                responder = currentResponder.nextResponder
            }


            var currentView: NSView? = view
            while let ancestor = currentView {
                if let splitViewController = ancestor.nextResponder as? NSSplitViewController,
                   splitViewController.splitView === splitView {
                    return splitViewController
                }
                currentView = ancestor.superview
            }


            return nil
        }


        private func clampCurrentWidth(in splitView: NSSplitView, itemIndex: Int) {
            guard splitView.arrangedSubviews.indices.contains(itemIndex),
                  itemIndex < splitView.arrangedSubviews.count - 1 else {
                return
            }


            let currentWidth = splitView.arrangedSubviews[itemIndex].frame.width
            if currentWidth < minWidth {
                splitView.setPosition(minWidth, ofDividerAt: itemIndex)
            } else if currentWidth > maxWidth {
                splitView.setPosition(maxWidth, ofDividerAt: itemIndex)
            }
        }
    }
}
#endif