How to align slider labels in SwiftUI

On macOS we can add labels directly to SwiftUI slider. Has someone found a way how we can align two or more sliders in a VStack?


- label text right aligned

- all labels should have the same width (given by the largest text)

- all slider bars should have the same but flexible width within the containers frame

  • This is so exceedingly complex for such a simple problem…

Add a Comment

Accepted Reply

This requires the a similar approach to one I outlined in an answer to an earlier question. The current best source of information on this from inside Apple is WWDC 2019 session 237, starting around the 19:35 mark, where they talk about defining custom alignments.


Now, there's evidently an alignment guide that's set by the system to refer to the leading edge of the control itself, even when displaying a label. This is how, on macOS, you get the alignment behavior you describe from items within a Form. That alignment guide value isn't public, though, so we can't take advantage of it directly. Instead, we'll have to either implement the label separately (so we can get the coordinate of the leading edge of the control) or always define the label using a ViewBuilder, so we can associate our guide with the trailing edge of the label.


What you need to do is to create a new custom alignment value, and tell your VStack to align its content using that. Then, within the VStack, you set the value for this new alignment to be the leading edge of your slider or the trailing edge of your text. You set the alignment value using the .alignmentGuide() modifier on the view whose coordinates you need: this would be the slider, if you're using its leading edge, or the label to use its trailing edge. Here we'll use the trailing edge of the label, which we'll define using a ViewBuilder rather than the StringProtocol/LocalizedStringKey initializer parameter; that will let us attach the guide to the Text view we use for the label's content.


The aligment itself is a custom type, but one that's very straightforward to make. You're going to create an extension on HorizontalAlignment to define a new static value there. That value will initialize a new HorizontalAlignment value using an identifier you also specify. The identifier type needs to conform to AlignmentID, which merely means it'll define a default value to use when one isn't specified manually via the .alignmentGuide() view modifier:


extension HorizontalAlignment {
    private enum ControlLeadingEdgeAlignment: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[.center] // default is center, but could be anything really.
        }
    }
    static let controlLeadingEdge = HorizontalAlignment(ControlLeadingEdgeAlignment.self)
}


With this in place, you'll use your new .controlLeadingEdge alignment in two places. First, you'll supply it as the alignment argument to your VStack's initializer. Then, you'll attach a .alignmentGuide() view modifier to each of your sliders' labels, explicitly defining its value as equal to '.trailing'. Because this alignment guide doesn't exist on the HStack type, the value from your slider will bubble up through that to be seen by the VStack. Note that this doesn't work with built-in guides: you can't just redefine '.trailing' this way, as the HStack already has its own idea what that means, and will 'override' yours.


This can be broken out into a View type quite neatly to always define that layout guide with minimal effort, or you can simply do everything inline. Here's an example of how you'd use it:


VStack(alignment: .controlLeadingEdge) { // align subviews by aligning guides for '.controlLeadingEdge'
    HStack {
        Slider(value: $value1) {
            Text("Title")
                .font(.headline)
                .alignmentGuide(.controlLeadingEdge) { $0[.trailing] } // .controlLeadingEdge == label.trailing
        }
        Slider(value: $value2) {
            Text("Much Longer Title")
                .font(.headline)
                .alignmentGuide(.controlLeadingEdge) { $0[.trailing] } // .controlLeadingEdge == label.trailing
        }
    }
}

Replies

Did you put them all in a HStack and set frame for labels, like this :


struct ContentView3: View {
    @State var sliderValue = 0.0
    var minimumValue = 0.0
    var maximumvalue = 100.0
    @State var sliderValue2 = 0.0
    var minimumValue2 = -100.0
    var maximumvalue2 = 1000.0
   
    var body: some View {
        VStack {
           
            HStack {
                Text("\(Int(minimumValue))")
                    .frame(minWidth: 60, maxWidth: 60, minHeight: 20, maxHeight: 20)
          
                Slider(value: $sliderValue, in: minimumValue...maximumvalue)
                Text("\(Int(maximumvalue))")
                    .frame(minWidth: 60, maxWidth: 60, minHeight: 20, maxHeight: 20)
            }
              .padding()
           
            Text("\(Int(sliderValue))")
           
            HStack {
                Text("\(Int(minimumValue2))")
                    .frame(minWidth: 60, maxWidth: 60, minHeight: 20, maxHeight: 20)
              
                Slider(value: $sliderValue2, in: minimumValue2...maximumvalue2)
                Text("\(Int(maximumvalue2))")
                    .frame(minWidth: 60, maxWidth: 60, minHeight: 20, maxHeight: 20)
            }
               .padding()
            
            Text("\(Int(sliderValue2))")
        }
    }
}

This requires the a similar approach to one I outlined in an answer to an earlier question. The current best source of information on this from inside Apple is WWDC 2019 session 237, starting around the 19:35 mark, where they talk about defining custom alignments.


Now, there's evidently an alignment guide that's set by the system to refer to the leading edge of the control itself, even when displaying a label. This is how, on macOS, you get the alignment behavior you describe from items within a Form. That alignment guide value isn't public, though, so we can't take advantage of it directly. Instead, we'll have to either implement the label separately (so we can get the coordinate of the leading edge of the control) or always define the label using a ViewBuilder, so we can associate our guide with the trailing edge of the label.


What you need to do is to create a new custom alignment value, and tell your VStack to align its content using that. Then, within the VStack, you set the value for this new alignment to be the leading edge of your slider or the trailing edge of your text. You set the alignment value using the .alignmentGuide() modifier on the view whose coordinates you need: this would be the slider, if you're using its leading edge, or the label to use its trailing edge. Here we'll use the trailing edge of the label, which we'll define using a ViewBuilder rather than the StringProtocol/LocalizedStringKey initializer parameter; that will let us attach the guide to the Text view we use for the label's content.


The aligment itself is a custom type, but one that's very straightforward to make. You're going to create an extension on HorizontalAlignment to define a new static value there. That value will initialize a new HorizontalAlignment value using an identifier you also specify. The identifier type needs to conform to AlignmentID, which merely means it'll define a default value to use when one isn't specified manually via the .alignmentGuide() view modifier:


extension HorizontalAlignment {
    private enum ControlLeadingEdgeAlignment: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[.center] // default is center, but could be anything really.
        }
    }
    static let controlLeadingEdge = HorizontalAlignment(ControlLeadingEdgeAlignment.self)
}


With this in place, you'll use your new .controlLeadingEdge alignment in two places. First, you'll supply it as the alignment argument to your VStack's initializer. Then, you'll attach a .alignmentGuide() view modifier to each of your sliders' labels, explicitly defining its value as equal to '.trailing'. Because this alignment guide doesn't exist on the HStack type, the value from your slider will bubble up through that to be seen by the VStack. Note that this doesn't work with built-in guides: you can't just redefine '.trailing' this way, as the HStack already has its own idea what that means, and will 'override' yours.


This can be broken out into a View type quite neatly to always define that layout guide with minimal effort, or you can simply do everything inline. Here's an example of how you'd use it:


VStack(alignment: .controlLeadingEdge) { // align subviews by aligning guides for '.controlLeadingEdge'
    HStack {
        Slider(value: $value1) {
            Text("Title")
                .font(.headline)
                .alignmentGuide(.controlLeadingEdge) { $0[.trailing] } // .controlLeadingEdge == label.trailing
        }
        Slider(value: $value2) {
            Text("Much Longer Title")
                .font(.headline)
                .alignmentGuide(.controlLeadingEdge) { $0[.trailing] } // .controlLeadingEdge == label.trailing
        }
    }
}

Hi Jim,


this solved the problem. I hope Apple will give us some additional alignments like .leftControlAlignment and .rightControlAlignment similar to .firstTextBaseline and .lastTextBaseline that we already have for Text(). Then we don't need to use custom alignment guides for all the controls with labels. But this works perfect. Thank you for your good explanation.