Search code examples
swiftswiftuicgaffinetransform

How to add animation on CGAffineTransform for rotationAngle in SwiftUI?


I need to animate a CGAffineTransform with a rotationAngle but I don't know if is possible because CGAffineTransform is not conform to Animatable. Maby with "withAnimation", but I don't understand how to use this. I know I can use rotationEffect but for some raisons I must to use CGAffineTransform.

My code:

import SwiftUI
import CoreLocation

struct Arrow: View {

  var locationManager = CLLocationManager()
  @ObservedObject var location: LocationManager = LocationManager()

  private var animationArrow: Animation {
    Animation
      .easeInOut(duration: 0.2)
  }

  var body: some View {
    VStack {
      Image("arrow")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 300, height: 300)
        .transformEffect(
          CGAffineTransform(translationX: -150, y: -150)
            .concatenating(CGAffineTransform(rotationAngle: CGFloat(-location.heading.degreesToRadians)))
            .concatenating(CGAffineTransform(translationX: 150, y: 150))
        )
        .animation(animationArrow)

      Text(String(location.heading.degreesToRadians))
        .font(.system(size: 20))
        .fontWeight(.light)
        .padding(.top, 15)

      Text(String(location.coordinates[0]))
        .font(.system(size: 20))
        .fontWeight(.light)
        .padding(.top, 15)

      Text(String(location.coordinates[1]))
        .font(.system(size: 20))
        .fontWeight(.light)
        .padding(.top, 15)
    }
  }
}

Solution

  • If you cannot use any of the existing animatable effects, you need to teach SwiftUI how to animate your transform matrix. You do that with a GeometryEffect. It is a protocol that conforms both to Animatable and ViewModifier.

    If you want a full explanation on how GeometryEffect works, check my post: https://swiftui-lab.com/swiftui-animations-part2/

    Here's a working example:

    struct ContentView: View {
        @State private var animate = false
    
        var body: some View {
            Rectangle()
                .frame(width: 50, height: 50)
                .modifier(MyEffect(angle: animate ? .pi * 2 : 0))
                .onTapGesture {
                    withAnimation(.easeInOut(duration: 2.0)) {
                        self.animate.toggle()
                    }
            }
        }
    }
    
    struct MyEffect: GeometryEffect {
        var angle: CGFloat
    
        var animatableData: CGFloat {
            get { angle }
            set { angle = newValue }
        }
    
        func effectValue(size: CGSize) -> ProjectionTransform {
            return ProjectionTransform(CGAffineTransform(rotationAngle: angle))
        }
    }
    

    Responding to your comments, you can use withAnimation with any variable, not only booleans. If the variable ends up affecting an animatable parameter, it will animate. Here's an example:

    struct ContentView: View {
        @State private var angle: CGFloat = 0
    
        var body: some View {
            VStack {
                Rectangle()
                    .frame(width: 50, height: 50)
                    .modifier(MyEffect(angle: angle))
    
                Button("Go to 0") {
                    withAnimation(.easeInOut(duration: 2.0)) {
                        self.angle = 0
                    }
                }
    
                Button("Go to 360") {
                    withAnimation(.easeInOut(duration: 2.0)) {
                        self.angle = .pi * 2
                    }
                }
            }
        }
    }