Created
October 17, 2019 16:00
-
-
Save leoMehlig/9253ad3d5586d5e6f3097bf8dbe17cf6 to your computer and use it in GitHub Desktop.
Keyframe animations in SwiftUI
This file contains hidden or 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
// | |
// Keyframes.swift | |
// Watch Extension | |
// | |
// Created by Leo Mehlig on 17.10.19. | |
// Copyright © 2019 Asana Rebel GmbH. All rights reserved. | |
// | |
import SwiftUI | |
/// One keyframe in the animation. | |
public struct Keyframe<V: VectorArithmetic>: Equatable { | |
/// The value of the keyfame. | |
public let value: V | |
/// The point in time where the `value` is reached, must be in range [0, 1]. | |
public let progress: Double | |
/// Initialize with a value and progress. | |
public init(value: V, progress: Double) { | |
self.value = value | |
self.progress = progress | |
} | |
} | |
public struct KeyframeModifier<M: AnimatableModifier>: AnimatableModifier { | |
/// The array of keyframes. | |
public let keyframes: [Keyframe<M.AnimatableData>] | |
/// The `AnimatableModifier`. | |
public let modifier: M | |
/// The current progress. Must be in range [0, 1]. | |
private var progress: Double | |
public var animatableData: Double { | |
get { progress } | |
set { progress = newValue } | |
} | |
/// Initialize a new `KeyframeModifier`. | |
/// - Parameter keyframes: An array of the keyframes. Does not have to be sorted. | |
/// - Parameter modifier: The modifier used to apply the changes. | |
/// - Parameter progress: The progress of the animation. Use a `@State` variable of value `0` and set to `1` to start animation. | |
public init(keyframes: [Keyframe<M.AnimatableData>], modifier: M, progress: Double) { | |
self.keyframes = keyframes.sorted(by: { $0.progress <= $1.progress }) | |
self.modifier = modifier | |
self.progress = progress | |
} | |
/// Initialize a new `KeyframeModifier`. | |
/// - Parameter keyframes: An array of values which will be used as equally distributed keyframes. | |
/// - Parameter modifier: The modifier used to apply the changes. | |
/// - Parameter progress: The progress of the animation. Use a `@State` variable of value `0` and set to `1` to start animation. | |
public init(values: [M.AnimatableData], modifier: M, progress: Double) { | |
let frames = values.enumerated() | |
.map { Keyframe(value: $1, progress: 1 / Double(values.count - 1) * Double($0)) } | |
self.init(keyframes: frames, modifier: modifier, progress: progress) | |
} | |
public func body(content: Content) -> some View { | |
let progress = min(max(self.progress, 0), 1) | |
let prev = keyframes.last(where: { $0.progress <= progress })! | |
let next = keyframes.first(where: { $0.progress >= progress })! | |
var m = modifier | |
if prev != next { | |
let factor = 1 / (next.progress - prev.progress) | |
var val1 = next.value | |
val1.scale(by: (progress - prev.progress) * factor) | |
var val2 = prev.value | |
val2.scale(by: (next.progress - progress) * factor) | |
m.animatableData = val1 + val2 | |
} else { | |
m.animatableData = prev.value | |
} | |
return content.modifier(m) | |
} | |
} | |
extension View { | |
/// Creates a keyframe animation. Use an `.animation` modifier to specify the animation. | |
/// - Parameter keyframes: The array of keyframes. Does not have to be sorted. | |
/// - Parameter progress: The progress of the animation. Use a `@State` variable of value `0` and set to `1` to start animation. | |
/// - Parameter modifier: The modifier used to apply the changes. | |
public func keyframes<M: AnimatableModifier>(_ keyframes: [Keyframe<M.AnimatableData>], | |
progress: Double, | |
modifier: M) -> some View { | |
return self.modifier(KeyframeModifier(keyframes: keyframes, modifier: modifier, progress: progress)) | |
} | |
/// Creates a keyframe animation by equally distributing the `values`. Use an `.animation` modifier to specify the animation. | |
/// - Parameter values: The values of the animation. | |
/// - Parameter progress: The progress of the animation. Use a `@State` variable of value `0` and set to `1` to start animation. | |
/// - Parameter modifier: The modifier used to apply the changes. | |
public func keyframes<M: AnimatableModifier>(_ values: [M.AnimatableData], | |
progress: Double, | |
modifier: M) -> some View { | |
return self.modifier(KeyframeModifier(values: values, modifier: modifier, progress: progress)) | |
} | |
} | |
/// An example of a `AnimatableModifier` to animate the `.opacity` of a `View ` in an keyframe animation. | |
private struct OpacityModifier: AnimatableModifier { | |
var animatableData: Double = .zero | |
func body(content: Content) -> some View { | |
return content.opacity(animatableData) | |
} | |
} | |
extension View { | |
/// Create a keyframe animation of the opacity of the view. Use an `.animation` modifier to specify the animation. | |
/// - Parameter keyframes: The array of keyframes. Does not have to be sorted. | |
/// - Parameter progress: The progress of the animation. Use a `@State` variable of value `0` and set to `1` to start animation. | |
public func keyframes(opacity keyframes: [Keyframe<Double>], progress: Double) -> some View { | |
return self.keyframes(keyframes, progress: progress, modifier: OpacityModifier()) | |
} | |
/// Create a keyframe animation of the opacity of the view by equally distributing the `values`. Use an `.animation` modifier to specify the animation. | |
/// - Parameter keyframes: The opacity values of the animation. | |
/// - Parameter progress: The progress of the animation. Use a `@State` variable of value `0` and set to `1` to start animation. | |
public func keyframes(opacity values: [Double], progress: Double) -> some View { | |
return self.keyframes(values, progress: progress, modifier: OpacityModifier()) | |
} | |
} |
This file contains hidden or 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
import SwiftUI | |
/// Example of using keyframe animations in SwiftUI | |
struct LoadingView: View { | |
// This represents the progress of the keyframe animation. | |
// Set to `1` to play the complet animation. | |
@State private var progress: Double = 0 | |
var body: some View { | |
HStack(alignment: .center, spacing: 10) { | |
// Bugs of a bug in SwiftUI, `AnimatableModifier` need to be framed into an overlay | |
// https://swiftui-lab.com/animatablemodifier-inside-containers-bug/ | |
point | |
.foregroundColor(.clear) | |
.overlay(point.keyframes(opacity: [0, 1, 1, 0, 0], progress: progress)) | |
point | |
.foregroundColor(.clear) | |
.overlay(point.keyframes(opacity: [0, 0, 1, 1, 0], progress: progress)) | |
point | |
.foregroundColor(.clear) | |
.overlay(point.keyframes(opacity:[0, 0, 0, 1, 1], progress: progress)) | |
} | |
.onAppear { | |
// Specify animation and set progress to `1` to start animation. | |
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) { | |
self.progress = 1 | |
} | |
} | |
} | |
var point: some View { | |
Circle() | |
.foregroundColor(Color.white) | |
.aspectRatio(1, contentMode: .fit) | |
} | |
} | |
struct LoadingView_Previews: PreviewProvider { | |
static var previews: some View { | |
LoadingView() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment