swiftuilayout

How to rotate an item in SwiftUI and *then* lay out


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 Sliders always take all of the available room. Which is precisely what I want the vertical ones to do.


Solution

  • 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")
            }
        }
    }
    

    Screenshot

    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.