skip to Main Content

I had a fixed View RectangleView that displays content from external sources with arbitrary width and height which means that direct adjustments of frame is not preferred with the .frame modifier. When user clicks on a button to rotate this View, the view rotates as expected but I wanted the horizontally rotated RectangleView to fit into the screen and not overflow outside of the bounds of display.

// Playground
  
import SwiftUI
import PlaygroundSupport

struct RectangleView: View {
    @Binding var rotation: Double

    var body: some View {
        Rectangle()
            .fill(Color.red)
            .frame(width: 100, height: 200) // fixed
            .rotationEffect(.degrees(rotation))
    }
}

struct ContentView: View {
    @State public var rotation: Double = 0

    var body: some View {
        VStack {
            RectangleView(rotation: $rotation)

            Button(action: {
                withAnimation {
                    rotation += 90
                }
            }) {
                Text("Rotate")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())




I had tried using .aspectRatio(contentMode: .fit), or a GeometryReader to make the rectangle fit the screen on 90 or 270 degrees but nothing really works. How to solve this issue? enter image description here

2

Answers


  1. Problem is that you are rotating and modifiying the shape frame but you need to modify the View frame instead, you need to control when your view is in horizontal or vertical state and adjust maxHeight accordingly

    import SwiftUI
    
    struct RectangleView: View {
        @Binding var rotation: Double
        //@State var affectedFrameDimension: Dimension
    
        var body: some View {
            Rectangle()
                .fill(Color.red)
                //.frame(maxWidth: maxWidht)
                //.rotationEffect(.degrees(rotation))
                .clipped()
                .border(Color.black)
        }
    }
    
    struct ContentView: View {
        @State public var rotation: Double = 0
        @State public var vertical: Bool = true
    
        var body: some View {
            GeometryReader(content: { geometry in
                VStack {
                    RectangleView(rotation: $rotation)
                        .rotationEffect(.degrees(rotation))
                        .frame(maxHeight: vertical ? geometry.size.height : geometry.size.width )
    
                    Button(action: {
                        withAnimation {
                            rotation += 90
                            vertical.toggle()
                        }
                    }) {
                        Text("Rotate")
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(10)
                    }
                }
            })
        }
    }
    
    #Preview {
        ContentView()
    }
    

    Result

    enter image description here

    Login or Signup to reply.
  2. I am assuming that the content represented by the red rectangle might be any size and that this size is not known to the presenting view. You just want it to be scaled to fit, whatever size it is. The aspect ratio of the content should also be maintained.

    The modifier .scaledToFit can be applied to any View and this is a convenient way to make a view fit into the available space. However, it only works if:

    1. a fixed width or height has not been applied to the view
    2. a transformation (such as rotation) has not been applied to the view.

    In the case here, neither of the conditions are satisfied, so .scaledToFit needs some help to work.

    • To resolve the first issue, the frame size can be set using ìdealWidth and idealHeight, instead of width and height.

    • To resolve the second issue, the layout needs to be aware of the rotation. A custom Layout can be used for this purpose.

    The example below uses a custom layout called MaxRotatedContent. This is only expecting one subview. If the subview has been rotated then the angle of rotation should be made known to the layout using the modifier .layoutValue and key RotationDegrees.

    The custom layout computes the size of the rotated subview using the formula that can be found in the answer to How to scale a rotated rectangle to always fit another rectangle. This works for any angle of rotation. However, if you are only going to be rotating the view by exact multiples of 90 degrees then the calculation could be simplified.

    BTW, the angle of rotation doesn’t need to be supplied to RectangleView as a Binding , because it is read-only. So you can just define the variable using let instead.

    struct RotationDegrees: LayoutValueKey {
        static let defaultValue = Double.zero
    }
    
    struct RectangleView: View {
        let rotation: Double
    
        var body: some View {
            Rectangle()
                .fill(Color.red)
    //            .frame(width: 100, height: 200) // fixed
                .frame(idealWidth: 100, idealHeight: 200)
                .rotationEffect(.degrees(rotation))
        }
    }
    
    struct MaxRotatedContent: Layout {
    
        public func sizeThatFits(
            proposal: ProposedViewSize,
            subviews: Subviews,
            cache: inout ()
        ) -> CGSize {
            proposal.replacingUnspecifiedDimensions()
        }
    
        public func placeSubviews(
            in bounds: CGRect,
            proposal: ProposedViewSize,
            subviews: Subviews,
            cache: inout ()
        ) {
            if let firstSubview = subviews.first {
    
                // Credit to M Oehm for the formula used here
                // https://stackoverflow.com/a/33867165/20386264
                let subviewSize = firstSubview.sizeThatFits(proposal)
                let w = subviewSize.width
                let h = subviewSize.height
                let rotationDegrees = firstSubview[RotationDegrees.self]
                let rotationRadians = rotationDegrees * .pi / 180
                let absSin = abs(sin(rotationRadians))
                let absCos = abs(cos(rotationRadians))
                let W = (w * absCos) + (h * absSin)
                let H = (w * absSin) + (h * absCos)
                let scalingFactor = min((bounds.width / W), (bounds.height / H))
                let scaledWidth = scalingFactor * w
                let scaledHeight = scalingFactor * h
                let x = bounds.minX + (bounds.width - scaledWidth) / 2
                let y = bounds.minY + (bounds.height - scaledHeight) / 2
                firstSubview.place(
                    at: CGPoint(x: x, y: y),
                    proposal: .init(width: scaledWidth, height: scaledHeight)
                )
            }
        }
    }
    
    struct ContentView: View {
        @State public var rotation: Double = 0
    
        var body: some View {
            VStack {
    
                MaxRotatedContent {
                    RectangleView(rotation: rotation)
                        .scaledToFit()
                        .layoutValue(key: RotationDegrees.self, value: rotation)
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .border(.black)
    
                Slider(value: $rotation, in: 0...360)
                    .padding()
            }
        }
    }
    

    Animation

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