Skip to content

Instantly share code, notes, and snippets.

@leoMehlig
Created October 17, 2019 16:00
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leoMehlig/9253ad3d5586d5e6f3097bf8dbe17cf6 to your computer and use it in GitHub Desktop.
Save leoMehlig/9253ad3d5586d5e6f3097bf8dbe17cf6 to your computer and use it in GitHub Desktop.
Keyframe animations in SwiftUI
//
// 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())
}
}
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