1

I have two Rectangles, Rectangle A and Rectangle B. Rectangle A will move horizontally, and the center point of A will align with the top left corner of B in the horizontal direction. B will rotate around its own center point.

enter image description here

I have some code but I don't know how to writing next. How can I use alignmentGuide and PreferenceKey to achieve this animation?

struct MyAnchorKey: PreferenceKey {
    static var defaultValue: Anchor<CGPoint>? = nil

    static func reduce(value: inout Anchor<CGPoint>?, nextValue: () -> Anchor<CGPoint>?) {
        value = value ?? nextValue()
    }
}


struct ContentView: View {
    @State private var rotationAngle: Double = 0

    var body: some View {
        VStack(spacing: 50) {
            // Rectangle A
            Rectangle()
                .fill(Color.blue)
                .frame(width: 50, height: 50)
                // How to use alignmentGuide to align the center point with the top left corner of B
            // Rectangle B
            Rectangle()
                .fill(Color.gray)
                .frame(width: 200, height: 50)
                .rotationEffect(.degrees(rotationAngle))
                .animation(Animation.linear(duration: 3).repeatForever(autoreverses: false))
                // How to use anchorPreference to record top leading position
                
        }
        .onAppear {
            rotationAngle = 360
        }
    }
}
mars
  • 109
  • 5
  • 21

1 Answers1

1

Edit: cleaned up

The main complication here is that the displacement of Rectangle A is going to need to be computed from the rotation angle using trigonometry. This means, if you want it to be animated, you are going to need an Animatable view modifier to perform the computation.

There is also a problem with the width of the VStack stretching. When an alignment guide is used, the overall width of the VStack is going to change when Rectangle A is at the limits of its movement. This stretching of the width will have an impact on the position of Rectangle B too. However, this can be resolved quite easily by adding horizontal padding to Rectangle B, to allow space for the movement of Rectangle A.

This seems to work:

struct PositionModifier: ViewModifier, Animatable {

    let halfDiagonal: CGFloat
    let startAngle: CGFloat
    var rotationAngle: CGFloat

    init(sizeB: CGSize, rotationAngle: CGFloat) {
        let w = sizeB.width
        let h = sizeB.height
        self.halfDiagonal = sqrt((w * w) + (h * h)) / 2
        self.startAngle = atan(-w / h)
        self.rotationAngle = rotationAngle
    }

    /// Implementation of protocol property
    var animatableData: CGFloat {
        get { rotationAngle }
        set { rotationAngle = newValue }
    }

    func body(content: Content) -> some View {
        content.alignmentGuide(HorizontalAlignment.center, computeValue: { d in
            d.width / 2 -
            (halfDiagonal * sin((rotationAngle * Double.pi / 180) + startAngle))
        })
    }
}

struct ContentView: View {

    private let sizeA = CGSize(width: 50, height: 50)
    private let sizeB = CGSize(width: 200, height: 50)
    @State private var rotationAngle: Double = 0

    var body: some View {
        VStack(alignment: .center, spacing: 50) {
            // Rectangle A
            Rectangle()
                .fill(Color.blue)
                .frame(width: sizeA.width, height: sizeA.height)
                .modifier(PositionModifier(sizeB: sizeB, rotationAngle: rotationAngle))

            // Rectangle B
            Rectangle()
                .fill(Color.gray)
                .frame(width: sizeB.width, height: sizeB.height)
                .padding(.horizontal, sizeA.width / 2)
                .rotationEffect(.degrees(rotationAngle))
        }
        .animation(Animation.linear(duration: 3).repeatForever(autoreverses: false), value: rotationAngle)
        .onAppear {
            rotationAngle = 360
        }
    }
}

An alternative and perhaps simpler approach is to set an x-offset on Rectangle A, instead of using an alignment guide. Just change the body of the view modifier to the following:

    func body(content: Content) -> some View {
        content.offset(x:
            halfDiagonal * sin((rotationAngle * Double.pi / 180) + startAngle)
        )
    }

The padding on Rectangle B can be removed too.

Benzy Neez
  • 1,546
  • 2
  • 3
  • 10