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.
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
}
}
}
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.