Skip to content

Instantly share code, notes, and snippets.

@gbuela
Created April 27, 2023 16:39
Show Gist options
  • Save gbuela/70c6123762ab105ecfd277e5d9da73d9 to your computer and use it in GitHub Desktop.
Save gbuela/70c6123762ab105ecfd277e5d9da73d9 to your computer and use it in GitHub Desktop.
RotatingView
import SwiftUI
struct PausableRotation: GeometryEffect {
@Binding var currentAngle: Double
private var currentAngleValue: Double = 0.0
var animatableData: Double {
get { currentAngleValue }
set { currentAngleValue = newValue }
}
init(desiredAngle: Double, currentAngle: Binding<Double>) {
self.currentAngleValue = desiredAngle
self._currentAngle = currentAngle
}
func effectValue(size: CGSize) -> ProjectionTransform {
DispatchQueue.main.async {
self.currentAngle = currentAngleValue
}
let xOffset = size.width / 2
let yOffset = size.height / 2
let transform = CGAffineTransform(translationX: xOffset, y: yOffset)
.rotated(by: currentAngleValue)
.translatedBy(x: -xOffset, y: -yOffset)
return ProjectionTransform(transform)
}
}
struct RotatingView<RV: View>: View {
@State private var desiredAngle: Double = 0.0
@State private var currentAngle: Double = 0.0
@State private var isRotationInEffect = false
let isRotating: Bool
let completeTurnInterval: TimeInterval
let accelerationInterval: TimeInterval
let accelerationRadians: Double
@ViewBuilder let content: RV
private var foreverAnimation: Animation {
Animation.linear(duration: completeTurnInterval)
.repeatForever(autoreverses: false)
}
var body: some View {
Group {
if isRotating {
content
.modifier(PausableRotation(desiredAngle: desiredAngle, currentAngle: $currentAngle))
} else {
content
.modifier(PausableRotation(desiredAngle: desiredAngle, currentAngle: $currentAngle))
}
}
.onAppear {
// when it's expected to show up rotating right away without accelerating from 0
if isRotating && !isRotationInEffect {
isRotationInEffect = true
let startAngle = currentAngle.truncatingRemainder(dividingBy: Double.pi * 2)
desiredAngle = startAngle
withAnimation(foreverAnimation) {
self.desiredAngle = startAngle + Double.pi * 2
}
}
}
.onChange(of: isRotating) { isRotating in
if isRotating {
let startAngle = currentAngle
withAnimation(.easeIn(duration: accelerationInterval)) {
self.desiredAngle = startAngle + accelerationRadians
}
DispatchQueue.main.asyncAfter(deadline: .now() + accelerationInterval) {
isRotationInEffect = true
let startAngle = currentAngle.truncatingRemainder(dividingBy: Double.pi * 2)
self.desiredAngle = startAngle
withAnimation(foreverAnimation) {
self.desiredAngle = startAngle + Double.pi * 2
}
}
} else {
isRotationInEffect = false
let startAngle = currentAngle.truncatingRemainder(dividingBy: Double.pi * 2)
let finalAngle = startAngle + accelerationRadians
desiredAngle = startAngle
withAnimation(.easeOut(duration: accelerationInterval)) {
self.desiredAngle = finalAngle
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment