skip to Main Content

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.

2

Answers


  1. 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

    Login or Signup to reply.
  2. Note that you are using Xcode Previews. Previews behave a little differently from running the app normally.

    If you run the app normally, the window sizes will be the same for both cases (whatever the macOS default window size is, or whatever is stored in your preferences), and you can resize both to the smallest size possible. The sizes you see is just a Xcode Preview quirk.

    Notice that without a Divider, you can still resize the window to as big as you want, and the HStack sits in the middle, without changing its height, and there are lots of blank space below and above. On the other hand, the Pickers will expand horizontally, the HStack‘s width can change.

    Preview tries to display your view without any of those surrounding blank space. This is why you get a window that nicely fits the HStack when there is no Divider. Notice that the preview window’s width is the default macOS window width. This is because the pickers are greedy and tries to take up as much horizontal space as possible – the preview has to stop it somewhere.

    Dividers are also greedy, and tries to expand vertically as much as possible. When there is a Divider, the Text all gets aligned at the top (because HStack(alignment: .top)) when the height of the HStack is greater than the height of the pickers combined. Also, there is none of the "surrounding blank space" above and below anymore, because there is always the Divider to fill that space. The preview chooses the macOS default height for windows (again, it has to stop the expansion somewhere), which is a larger number than the height of the pickers combined.

    Previews could have been designed to resize the window to its minimum size, but it wasn’t designed like that. I’d imagine lots of views would look rather cramped this way.

    I’m not sure there is a way for previews to show the minimum sized-window. You can force the preview to use a particular sized window though:

    #Preview(traits: .fixedLayout(width: 500, height: 100)) {
        ContentView()
    }
    

    You can also try putting .fixedSize on the HStack, but this resizes to its "ideal size", not its minimum size. The "ideal size" is still quite a lot larger than the minimum size.

    // you can also set the ideal size
    // .frame(idealHeight: 100)
    .fixedSize(horizontal: false, vertical: true)
    

    As for aligning the labels, I would use a Grid:

    Grid(alignment: .trailing, horizontalSpacing: 8, verticalSpacing: 8) {
        GridRow {
            Text("Paper Size:")
            Picker("", selection: .constant(1)) {
                // Dummy data for the picker...
                Text("Foo").tag(1)
            }
        }
            
        GridRow {
            Text("Margins:")
            Picker("", selection: .constant(1)) {
                Text("Bar").tag(1)
            }
        }
    }
    

    Side note: you can put .pickerStyle(.menu) on the HStack, instead of each picker. It applies that style to all the children.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search