I'd like to have vertical sliders. I know about .rotationEffect()
but this rotates the item after it has been laid out, meaning it will overlap other elements in the layout as well as take up space which it does not visually use.
I'd like to be able to do something like:
VStack {
Text("Some long title")
HStack {
VerticalSlider()
Slider()
VerticalSlider()
}
Text("Some long footer")
}
I would expect the two vertical sliders not to overlap either label and only take enough width to draw the slider, and stretch to the available height of the HStack
.
The regular (horizontal) slider should take most of the width of the HStack
.
In other words, this would generate an "H" shape of sliders bound by the HStack
and not interfering with other contents of the VStack
.
Is this possible? Easily? I've seen methods for rotating text and setting its size, but that relies on asking it for its unconstrained size where Slider
s always take all of the available room. Which is precisely what I want the vertical ones to do.
A standard layout container, such as an HStack
, only looks at the width and height of the items it is laying out. So in general, if a View
with rotated content is to be shown inside a stack and you want the layout to work without having to set the width and height explicitly, it must have a natural width and height that incorporates the rotation effect.
For a vertical slider that consists of a regular Slider
with rotation effect, one way to implement it would be to use a hidden placeholder that reserves the width and height needed, then show the Slider
in an overlay over the placeholder.
You said the two vertical sliders should...
only take enough width to draw the slider, and stretch to the available height of the HStack
A Slider
is a particularly awkward example, because I don't think there is an easy way to get the width of a slider button. So I resorted to a hard-coded constant. However, the height is easy, you can just set maxHeight: .infinity
on the placeholder:
struct VerticalSlider<V>: View where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
@Binding private var value: V
private let bounds: ClosedRange<V>
private let onEditingChanged: (Bool) -> Void
private let sliderButtonSize: CGFloat = 27
init(
value: Binding<V>,
in bounds: ClosedRange<V>,
onEditingChanged: @escaping (Bool) -> Void = { _ in }
) {
self._value = value
self.bounds = bounds
self.onEditingChanged = onEditingChanged
}
var body: some View {
Rectangle()
.frame(width: sliderButtonSize)
.frame(maxHeight: .infinity)
.hidden()
.overlay {
GeometryReader { proxy in
Slider(value: $value, in: bounds, onEditingChanged: onEditingChanged)
.frame(width: proxy.size.height)
.rotationEffect(.degrees(-90))
.offset(
x: (proxy.size.width - proxy.size.height) / 2,
y: (proxy.size.height - proxy.size.width) / 2
)
}
}
}
}
struct ContentView: View {
@State private var setting1 = 0.0
@State private var setting2 = 0.0
@State private var setting3 = 0.0
var body: some View {
VStack {
Text("Some long title")
HStack {
VerticalSlider(value: $setting1, in: 0...10)
Slider(value: $setting2, in: 0...10)
VerticalSlider(value: $setting3, in: 0...10)
}
Text("Some long footer")
}
}
}
If, on the other hand, all the items in the stack are being rotated, then a custom Layout
might be a better approach. For example, a custom Layout
would be a suitable way to lay out a collection of labels that all need to be shown at a particular angle, maybe sloped at 45 degrees.