Search code examples
swiftmacosswiftuidivider

SwiftUI Divider not sizing itself to fit parent view?


I have a fairly basic SwiftUI view heirarcy that I'm attempting to insert a divider into, however the divider is sizing itself to some obscure value and pushing the parent view out to meet it. Every other view in the heirarchy is behaving correctly and the parent view is wrapping the largest child, as expected.

I've searched and searched and just can't find any insight into this behaviour - so I'm asking you good folks for some thoughs.

View code:

Form {
    HStack(alignment: .top) {
        VStack(alignment: .trailing) {
            Picker("Paper Size:", selection: $viewModel.layoutParameters.paperSize) {
                ForEach(PrintSize.allCases, id: \.self) {
                    Text($0.description)
                }
            }
            .pickerStyle(.menu)
                    
            Picker(selection: $viewModel.layoutParameters.margin) {
                ForEach(Margin.allCases, id: \.self) {
                    Text($0.description)
                }
            } label: {
                Text("Margins:")
                    .padding(.leading, 15) // This is janky, find a better way to do it.
            }
            .pickerStyle(.menu)
        }
                
        Divider()
                
        Picker("Default Print Size:", selection: $viewModel.layoutParameters.printSize) {
            ForEach(PrintSize.allCases, id: \.self) {
                Text($0.description)
            }
        }
        .pickerStyle(.menu)
    }
}
.padding(8)

Exhibited behaviour with divider:
With divider

Without divider:
enter image description here

Bonus points if you can give me a way to nudge the Margins label over without explicitly applying padding.


Solution

  • Both the issues that you describe can be solved by using overlays.

    1. A Divider is greedy and expands to use all the space available, just like a Spacer. To constrain it to the height of the HStack, just put it in an overlay over the HStack. By default, the overlay will be aligned to the center of the HStack. This is perfect, because the two Picker will have equal widths, so the Divider will automatically be positioned in the middle between them. However, two extra steps are needed to get it looking right:
    • The orientation of a Divider is normally determined by the stack that contains it, or horizontal by detault. Since you want it to be vertical, it needs to be nested inside a dummy HStack.
    • You will probably want to increase the spacing that is used by the HStack, since there is now only one space between the two Picker.
    1. If you want the label for "Margins" to have the same width as the label for "Paper Size" then you can establish the footprint needed by using a hidden version of the longer label. Then show the shorter label in an overlay.

    Here is an updated version of your example with the two changes applied:

    Form {
        HStack(alignment: .top, spacing: 20) { // <- spacing added
            VStack(alignment: .trailing) {
                Picker("Paper Size:", selection: $viewModel.layoutParameters.paperSize) {
                    ForEach(PrintSize.allCases, id: \.self) {
                        Text($0.description)
                    }
                }
                .pickerStyle(.menu)
    
                Picker(selection: $viewModel.layoutParameters.margin) {
                    ForEach(Margin.allCases, id: \.self) {
                        Text($0.description)
                    }
                } label: {
                    Text("Paper Size:")
                        .hidden()
                        .overlay(alignment: .trailing) {
                            Text("Margins:")
                        }
    //                    .padding(.leading, 15) // This is janky, find a better way to do it.
                }
                .pickerStyle(.menu)
            }
    
    //        Divider()
    
            Picker("Default Print Size:", selection: $viewModel.layoutParameters.printSize) {
                ForEach(PrintSize.allCases, id: \.self) {
                    Text($0.description)
                }
            }
            .pickerStyle(.menu)
        }
        .overlay {
            HStack {
                Divider()
            }
        }
    }
    .padding(8)
    

    Screenshot