Exploring SwiftUI: Working with Rotate Gesture

When my sister saw this feature I worked on using the RotateGesture, she was amazed. Rotate the gradient to shift hues, and haptics that feel like you are moving a gear!

What's a RotateGesture?

As the name suggests, it is a gesture in SwiftUI to recognise when trying to rotate something with your fingers. Like when you twist a knob or turn a dial. This gesture can make your app feel more fun to use. There used to be a RotationGesture before with limited properties, which has been deprecated and replaced by RotateGesture instead.

public struct RotateGesture : Gesture {}

The structure has many properties to play around with:

time: Date: This is when the rotation happened. A timestamp. You can use it to track how long someone has been rotating something. It is handy for making smooth animations based on time.

rotation: Angle: This shows how much the user has rotated. Think of it like turning a dial. If the value is 30 degrees, the user turned it that much from where they started. It is probably the main thing you will use to update your view's rotation.

velocity: Angle: This tells you how fast the rotation is happening. The speed of the turn. You can use this to make your rotations feel more natural or add some cool effects based on how fast someone turns.

startAnchor: UnitPoint: This is where the rotation began. Imagine putting your finger on a spot to start turning. That spot is the startAnchor. It can be useful if you want different effects based on where the user starts their rotation.

startLocation: CGPoint: This is the exact point where the rotation began. While startAnchor is a relative position, startLocation gives you the precise coordinates. You might use this for more detailed calculations or effects.

Setting Up the Gesture

To use a RotateGesture, you can start by tracking the rotation:

  @State private var rotationAngle: Angle = .zero

This will hold the information about how much you have rotated in terms of degress.

Adding the Gesture to a View

Next, you add the gesture to a view in your app. Here is an example:

.gesture(
    RotateGesture()
        .updating($rotationState) { value, state, _ in
            state = value.rotation
            // Do something with the rotation here
        }
)

This code tells SwiftUI to watch for rotation gestures on your view. When it sees one, it updates the rotationState with new information.

Using the Rotation Data

You might want to rotate your view, change its color, or trigger another effect:

.rotationEffect(rotationState.rotation)

This line applies the rotation directly to your view. As you twist your fingers, the view will twist, too!

Adding Some Polish

To make your rotation feel smooth and natural, you can add animations:

withAnimation(.spring()) {
    // Apply your rotation effect here
}

This will make the rotation feel bouncy and responsive, like a real object.

RotateGestureView

Here is a demo view for you to play with the parameters:

import SwiftUI

struct RotateGestureView: View {
  @State private var angle: Angle = .degrees(0)
  @State private var velocity: Angle = .degrees(0)
  @State private var startLocation: CGPoint = .zero
  @State private var startAnchor: UnitPoint = .center

  private var rotateGesture: some Gesture {
    RotateGesture(minimumAngleDelta: .degrees(1))
      .onChanged { value in
        withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
          angle = value.rotation
        }
        velocity = value.velocity
        startLocation = value.startLocation
        startAnchor = value.startAnchor
      }
      .onEnded { _ in
        withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
          velocity = .degrees(0)
        }
      }
  }

  var body: some View {
    VStack {
      Rectangle()
        .fill(Color.indigo)
        .frame(width: 200, height: 200)
        .rotationEffect(angle)
        .cornerRadius(12)
        .gesture(rotateGesture)
        .padding()

      VStack(alignment: .leading, spacing: 10) {
        Text("Rotation: \(angle.degrees, specifier: "%.2f")°")
        Text("Velocity: \(velocity.degrees, specifier: "%.2f")°/s")
        Text("Start Location: (\(startLocation.x, specifier: "%.2f"), \(startLocation.y, specifier: "%.2f"))")
        Text("Start Anchor: (\(startAnchor.x, specifier: "%.2f"), \(startAnchor.y, specifier: "%.2f"))")
      }
      .padding()
      .background(Color(.secondarySystemBackground))
      .cornerRadius(12)
    }
    .padding()
  }
}

#Preview {
  RotateGestureView()
}

Color Shifts and Haptics

I took the creative route with the rotation gesture. Let's look at the magic I created with it:

    .gesture(
      RotateGesture()
        .onChanged { angle in
          self.rotationAngle = angle.rotation

          let dampingFactor = 0.008
          let hueShift = (angle.rotation.degrees * dampingFactor) / 360.0
          viewModel.hueShift = hueShift
          viewModel.applyHueShift()
          
          // Start rotation
          withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
            isRotating = true
          }
          
          // Check if we've rotated enough to trigger haptic feedback
          if abs(angle.rotation.degrees - previousRotation.degrees) > 10 {
            hapticManager.playGearShift()
            previousRotation = angle.rotation
          }
        }
        .onEnded { _ in
          // End rotation with a slight delay
          DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
              isRotating = false
            }
          }
          previousRotation = .zero
          
        }
    )

I first change the colour (hue) based on how much you have rotated—like turning a colour wheel! Then, I add a spring animation to make the rotation smooth and natural. Finally, I add haptic feedback (gear-shifting feeling) every 10 degrees of rotation.

Wrapping Up the Gesture

When it is done rotating, I want things to settle nicely:

.onEnded { value in
    // End rotation with a slight delay
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
            isRotating = false
        }
    }
    previousRotation = .zero
}

This code adds a tiny delay before ending the rotation and uses a spring animation to make the object seem to have some weight.

Moving Forward

I love writing posts because I started writing about RotationGesture only to realise it is deprecated and learned about the RotateGesture instead.

Here are some ideas to try:

  • Use the rotation speed to affect something in your app, like making faster rotations have a bigger effect.
  • Combine rotation with other gestures for even cooler interactions. Your imagination is the limit, amplified by LLMs.
  • Also, I want you to consider how to make your rotation work well for everyone, even people who might have trouble with touch gestures.

Experiment with different effects and see what feels right for your app.

Happy rotating!