Last active
August 25, 2024 17:51
-
-
Save swiftui-lab/43faecbec695511d907111237e7b9595 to your computer and use it in GitHub Desktop.
Examples for SwiftUI Blog Post (Advanced SwiftUI Animations - Part 6)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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