Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI .rotationEffect() framing and offsetting

Tags:

swift

swiftui

When applying .rotationEffect() to a Text, it rotates the text as expected, but its frame remains unchanged. This becomes an issue when stacking rotated views with non-rotated views, such as with a VStack of HStack, causing them to overlap.

I initially thought the rotationEffect would simply update the frame of the Text to be vertical, but this is not the case.

I've tried manually setting the frame size and (if needed, offsetting) the Text, which sort of works, but I don't like this solution because it requires some guessing and checking of where the Text will appear, how big to make the frame, etc.

Is this just how rotated text is done, or is there a more elegant solution to this?

struct TextAloneView: View {

    var body: some View {
        VStack {
            Text("Horizontal text")
            Text("Vertical text").rotationEffect(.degrees(-90))
        }
    }
}

Overlapping Text

like image 547
ezhou00 Avatar asked Oct 21 '19 21:10

ezhou00


1 Answers

You need to adjust the frame yourself in this case. That requires capturing what the frame is, and then applying the adjustment.

First, to capture the existing frame, create a preference, which is a system for passing data from child views to their parents:

private struct SizeKey: PreferenceKey {
    static let defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

extension View {
    func captureSize(in binding: Binding<CGSize>) -> some View {
        overlay(GeometryReader { proxy in
            Color.clear.preference(key: SizeKey.self, value: proxy.size)
        })
            .onPreferenceChange(SizeKey.self) { size in binding.wrappedValue = size }
    }
}

This creates a new .captureSize(in: $binding) method on Views.

Using that, we can create a new kind of View that rotates its frame:

struct Rotated<Rotated: View>: View {
    var view: Rotated
    var angle: Angle

    init(_ view: Rotated, angle: Angle = .degrees(-90)) {
        self.view = view
        self.angle = angle
    }

    @State private var size: CGSize = .zero

    var body: some View {
        // Rotate the frame, and compute the smallest integral frame that contains it
        let newFrame = CGRect(origin: .zero, size: size)
            .offsetBy(dx: -size.width/2, dy: -size.height/2)
            .applying(.init(rotationAngle: CGFloat(angle.radians)))
            .integral

        return view
            .fixedSize()                    // Don't change the view's ideal frame
            .captureSize(in: $size)         // Capture the size of the view's ideal frame
            .rotationEffect(angle)          // Rotate the view
            .frame(width: newFrame.width,   // And apply the new frame
                   height: newFrame.height)
    }
}

And for convenience, an extension to apply it:

extension View {
    func rotated(_ angle: Angle = .degrees(-90)) -> some View {
        Rotated(self, angle: angle)
    }
}

And now your code should work as you expect:

struct TextAloneView: View {

    var body: some View {
        VStack {
            Text("Horizontal text")
            Text("Vertical text").rotated()
        }
    }
}
like image 69
Rob Napier Avatar answered Nov 11 '22 15:11

Rob Napier