Skip to content

Instantly share code, notes, and snippets.

@swiftui-lab
Last active March 28, 2024 08:20
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save swiftui-lab/43faecbec695511d907111237e7b9595 to your computer and use it in GitHub Desktop.
Save swiftui-lab/43faecbec695511d907111237e7b9595 to your computer and use it in GitHub Desktop.
Examples for SwiftUI Blog Post (Advanced SwiftUI Animations - Part 6)
// Author: SwiftUI-Lab (swiftui-lab.com)
// Description: Advanced SwiftUI Animations - Part 6 Examples
// blog article: https://swiftui-lab.com/swiftui-animations-part6
import SwiftUI
struct ContentView: View {
@State var show: Int? = nil
var body: some View {
VStack(spacing: 20) {
if show == nil {
Button("**Example #1:** Linear Animation") { show = 1 }
Button("**Example #2:** Animation Context: Environment") { show = 2 }
Button("**Example #3:** Animation Context: Data Persistence") { show = 3 }
Button("**Example #4:** shouldMerge()") { show = 4 }
Button("**Example #5:** velocity()") { show = 5 }
Button("**Example #6:** Variable Speed") { show = 6 }
} else {
switch show {
case 1: Example1()
case 2: Example2()
case 3: Example3()
case 4: Example4()
case 5: Example5()
case 6: Example6()
default: EmptyView()
}
Button("Back") {
show = nil
}
}
}
.padding()
}
}
// ------- Example #1 -------
struct MyCustomLinearAnimation: CustomAnimation {
let duration: TimeInterval
func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
guard time < duration else { return nil }
return value.scaled(by: time/duration)
}
}
extension Animation {
static func myCustomLinear(duration: TimeInterval) -> Animation { Animation(MyCustomLinearAnimation(duration: duration)) }
static var myCustomLinear: Animation { Animation(MyCustomLinearAnimation(duration: 2.0)) }
}
struct Example1: View {
@State var animate: Bool = false
var body: some View {
Text("😵‍💫")
.font(.system(size: 100))
.rotationEffect(.degrees(animate ? 360 : 0))
.task {
withAnimation(.myCustomLinear.delay(1).repeatForever(autoreverses: false)) {
animate.toggle()
}
}
}
}
// ------- Example #2 -------
struct Example2: View {
@State var animate = false
@State var stop = false
var body: some View {
VStack {
Text("😬")
.font(.system(size: 100))
.offset(x: animate ? -3 : 3, y: animate ? -3 : 3)
.animation(.random, value: animate)
.task {
animate.toggle()
}
.environment(\.stopRandom, stop)
Button("Chill Man") {
stop.toggle()
}
}
}
}
extension Animation {
static var random: Animation { Animation(RandomAnimation()) }
}
struct RandomAnimation: CustomAnimation {
func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
guard !context.environment.stopRandom else { return nil }
return value.scaled(by: Double.random(in: 0...1))
}
}
extension EnvironmentValues {
var stopRandom: Bool {
get {
return self[StopRandomAnimationKey.self]
}
set {
self[StopRandomAnimationKey.self] = newValue
}
}
}
public struct StopRandomAnimationKey: EnvironmentKey {
public static let defaultValue: Bool = false
}
// ------- Example #3 -------
private struct RandomAnimationState<Value: VectorArithmetic>: AnimationStateKey {
var stopRequest: TimeInterval? = nil
static var defaultValue: Self { RandomAnimationState() }
}
extension AnimationContext {
fileprivate var randomState: RandomAnimationState<Value> {
get { state[RandomAnimationState<Value>.self] }
set { state[RandomAnimationState<Value>.self] = newValue }
}
}
extension Animation {
static func random(fade: Double = 1.0) -> Animation { Animation(RandomAnimationWithFade(fadeTime: fade)) }
}
struct RandomAnimationWithFade: CustomAnimation {
// time to fade randomness since stop starts to end of animation
let fadeTime: Double
func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
if context.environment.stopRandom { // animation stop requested
if context.randomState.stopRequest == nil {
context.randomState.stopRequest = time
}
let randomIntensity = (time - context.randomState.stopRequest!) / fadeTime
if randomIntensity > 1 { return nil }
return value.scaled(by: Double.random(in: randomIntensity...1))
} else {
return value.scaled(by: Double.random(in: 0...1))
}
}
}
struct Example3: View {
@State var animate = false
@State var stop = false
var body: some View {
VStack(spacing: 10) {
Text("😬")
.font(.system(size: 100))
.offset(x: animate ? 0 : 6, y: animate ? 0 : 6)
.animation(.random(fade: 2.0), value: animate)
.task {
animate.toggle()
}
.environment(\.stopRandom, stop)
Button("Chill Man!") {
stop.toggle()
if !stop { animate.toggle() }
}
}
}
}
// ------- Example #4 -------
extension Animation {
static func myLinear(merge: Bool, duration: Double = 2.0) -> Animation {
Animation(MyLinearAnimation(merge: merge, duration: duration))
}
}
struct MyLinearState<Value: VectorArithmetic>: AnimationStateKey {
var from: Value? = nil
var interruption: TimeInterval? = nil
static var defaultValue: Self { MyLinearState() }
}
extension AnimationContext {
var myLinearState: MyLinearState<Value> {
get { state[MyLinearState<Value>.self] }
set { state[MyLinearState<Value>.self] = newValue }
}
}
struct MyLinearAnimation: CustomAnimation {
let merge: Bool
let duration: TimeInterval
func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
guard time < duration + (context.myLinearState.interruption ?? 0) else { return nil }
if let v = context.myLinearState.from {
return v.interpolated(towards: value, amount: (time-context.myLinearState.interruption!)/duration)
} else {
return value.scaled(by: time/duration)
}
}
func shouldMerge<V>(previous: Animation, value: V, time: TimeInterval, context: inout AnimationContext<V>) -> Bool where V : VectorArithmetic {
guard merge else { return false }
context.myLinearState.from = previous.base.animate(value: value, time: time, context: &context)
context.myLinearState.interruption = time
return true
}
}
struct Example4: View {
@State var show = true
var body: some View {
VStack {
RectView(title: ".linear()", offset: show ? 0 : 500)
.animation(.linear(duration: 2.0), value: show)
.foregroundStyle(.primary, .green.gradient)
RectView(title: ".myLinear(merge: false)", offset: show ? 0 : 500)
.animation(.myLinear(merge: false, duration: 2.0), value: show)
.foregroundStyle(.primary, .blue.gradient)
RectView(title: ".myLinear(merge: true)", offset: show ? 0 : 500)
.animation(.myLinear(merge: true, duration: 2.0), value: show)
.foregroundStyle(.primary, .yellow.gradient)
Button("Animate") {
show.toggle()
}
.padding(.vertical, 30)
}
}
struct RectView: View {
let title: String
let offset: CGFloat
var body: some View {
HStack {
Text(title).frame(width: 200, alignment: .leading)
RoundedRectangle(cornerRadius: 20)
.fill(.secondary)
.frame(width: 70, height: 70)
.offset(x: offset)
}.offset(x: -150)
}
}
}
// ------- Example #5 -------
struct DemoAnimation1: CustomAnimation {
let duration: TimeInterval
func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
guard time < duration else { return nil }
let r = value.scaled(by: time/duration)
print("TIME: \(time)\nVALUE: \(value)\nRETURN: \(r)")
return r
}
func velocity<V>(value: V, time: TimeInterval, context: AnimationContext<V>) -> V? where V : VectorArithmetic {
return value.scaled(by: 1.0/duration)
}
}
struct DemoAnimation2: CustomAnimation {
let duration: TimeInterval
func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
guard time < duration else { return nil }
return value.scaled(by: time/duration)
}
func velocity<V>(value: V, time: TimeInterval, context: AnimationContext<V>) -> V? where V : VectorArithmetic {
return value.scaled(by: -5)
}
}
struct DemoAnimation3: CustomAnimation {
let duration: TimeInterval
func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
guard time < duration else { return nil }
return value.scaled(by: time/duration)
}
func velocity<V>(value: V, time: TimeInterval, context: AnimationContext<V>) -> V? where V : VectorArithmetic {
return value.scaled(by: 5)
}
}
extension Animation {
static func demoAnimation1(duration: TimeInterval) -> Animation { Animation(DemoAnimation1(duration: duration)) }
static func demoAnimation2(duration: TimeInterval) -> Animation { Animation(DemoAnimation2(duration: duration)) }
static func demoAnimation3(duration: TimeInterval) -> Animation { Animation(DemoAnimation3(duration: duration)) }
}
struct Example5: View {
@State var animate1: Bool = true
@State var animate2: Bool = true
@State var animate3: Bool = true
var body: some View {
VStack {
VStack {
Text("🥎")
.font(.system(size: 50))
.offset(x: animate1 ? 0 : -100)
Text("velocity = value.scaled(by: 1.0/duration)").padding(.bottom, 20)
Text("🥎")
.font(.system(size: 50))
.offset(x: animate2 ? 0 : -100)
Text("velocity = value.scaled(by: -5.0)").padding(.bottom, 20)
Text("🥎")
.font(.system(size: 50))
.offset(x: animate3 ? 0 : -100)
Text("velocity = value.scaled(by: 5.0)").padding(.bottom, 25)
}
HStack {
Button("Linear") {
withAnimation(.demoAnimation1(duration: 2.0)) { animate1.toggle() }
withAnimation(.demoAnimation2(duration: 2.0)) { animate2.toggle() }
withAnimation(.demoAnimation3(duration: 2.0)) { animate3.toggle() }
}
Button("Spring") {
withAnimation(.spring(duration: 2.0)) {
animate1.toggle()
animate2.toggle()
animate3.toggle()
}
}
}
}
}
}
// ------- Example #6 -------
struct AnimationSpeedKey: EnvironmentKey {
static let defaultValue: Double = 1.0
}
extension EnvironmentValues {
var animationSpeed: Double {
get { self[AnimationSpeedKey.self] }
set { self[AnimationSpeedKey.self] = newValue }
}
}
// Variable Speed State
struct VariableSpeedState<Value: VectorArithmetic>: AnimationStateKey {
// projectedDuration combines the duration of the animation with the speed and the remaining animation
// For example: If the animation was initialized with a duration of 2.0 seconds,
// and half-way (at 1.0 second elapased) the speed is changed from 1.0X to 0.5X, the projectedDuration would be 3.0 seconds
var projectedDuration: TimeInterval? = nil
// the percentage of animation that has been performed so far (0.0 at the begining, and 1.0 at the end)
var completion: Double = 0.0
// the time when the context was last updated
var lastTime: TimeInterval = 0.0
static var defaultValue: Self { VariableSpeedState() }
}
extension AnimationContext {
var variableSpeedState: VariableSpeedState<Value> {
get { state[VariableSpeedState<Value>.self] }
set { state[VariableSpeedState<Value>.self] = newValue }
}
}
// Variable Speed Animation
extension Animation {
static var variableSpeed: Animation { .variableSpeed(duration: 1.0) }
static func variableSpeed(duration: Double) -> Animation {
Animation(VariableSpeedAnimation(duration: duration))
}
}
struct VariableSpeedAnimation: CustomAnimation {
let duration: TimeInterval
func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
// End animation if fully completed
guard context.variableSpeedState.completion < 1.0 else {
return nil
}
// get speed from environment
let speed = context.environment.animationSpeed
if let projectedDuration = context.variableSpeedState.projectedDuration {
let deltaT = time - context.variableSpeedState.lastTime
let timeLeft = projectedDuration - time
let completion = context.variableSpeedState.completion
context.variableSpeedState.completion += ((1.0-completion) / (timeLeft / deltaT))
context.variableSpeedState.projectedDuration = time + ((duration / speed) * (1.0-completion))
} else {
// first pass
context.variableSpeedState.projectedDuration = (duration / speed)
context.variableSpeedState.completion = (time / (duration / speed))
}
// save time for next iteration
context.variableSpeedState.lastTime = time
return value.scaled(by: min(1.0, max(0.0, context.variableSpeedState.completion)))
}
func velocity<V>(value: V, time: TimeInterval, context: AnimationContext<V>) -> V? where V : VectorArithmetic {
if let projectedDuration = context.variableSpeedState.projectedDuration {
return value.scaled(by: 1.0 / projectedDuration)
} else {
let speed = context.environment.animationSpeed
return value.scaled(by: 1.0 / (duration / speed))
}
}
}
struct Example6: View {
@State var speed: Double = 1.0
@State var animate = true
var body: some View {
VStack(spacing: 20) {
Text("🎲")
.font(.system(size: 110))
.rotationEffect(.degrees(animate ? 0 : 360))
.animation(.variableSpeed(duration: 2.0).repeatForever(autoreverses: false), value: animate)
.environment(\.animationSpeed, speed)
.task { animate.toggle() }
Slider(value: $speed, in: 0...5).frame(width: 200)
Text("Speed = \(String(format: "%.1f", speed)) X")
}
.padding(20)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment