Skip to content

AnimationKit simplifies sequential animations in iOS and macOS apps with SwiftUI.

License

Notifications You must be signed in to change notification settings

NuPlay/AnimationKit

Repository files navigation

AnimationKit

iOS: 14.0+ macOS-11.0+

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/NuPlay/AnimationKit.git", .upToNextMinor(from: "1.0.0"))
]

Usage

When creating a simple view

import SwiftUI
import AnimationKit

struct AnimationKit_Test: View {
  var body: some View {
    VStack {
      AnimatedStack(preset: .default) {
        Text("Hello, World!")
          .font(.title)
        Text("This is Simple Stack View")
          .font(.title2)
        Rectangle()
          .fill(Color.blue)
          .frame(width: 100, height: 100)
      }
    }
  }
}

When using ForEach

import SwiftUI
import AnimationKit

struct AnimationKit_Test: View {
  @State private var emojiData: [String] = ["πŸš€", "πŸ›Έ", "πŸ›°", "🌌", "🌠", "🌟", "πŸŒ•", "🌍", "πŸͺ", "πŸŒ‘", "πŸŒ’", "πŸŒ“", "πŸŒ”", "πŸŒ–", "πŸŒ—", "🌘", "πŸŒ™", "🌚", "🌝", "🌞"]
  
  var body: some View {
    ScrollView {
      LazyVStack {
        AnimatedForEach(emojiData, preset: .list) { emoji in
          Text(emoji)
            .font(.system(size: 100))
        }
      }
    }
  }
}
import SwiftUI
import AnimationKit

struct AnimationKitGrid_Test: View {
  @State private var emojiData: [String] = ["πŸš€", "πŸ›Έ", "πŸ›°", "🌌", "🌠", "🌟", "πŸŒ•", "🌍", "πŸͺ", "πŸŒ‘", "πŸŒ’", "πŸŒ“", "πŸŒ”", "πŸŒ–", "πŸŒ—", "🌘", "πŸŒ™", "🌚", "🌝", "🌞"]
  let columns = [GridItem(.adaptive(minimum: 100))]
  
  var body: some View {
    ScrollView {
      LazyVGrid(columns: columns, spacing: 20) {
        AnimatedForEach(emojiData, preset: .grid) { emoji in
          Text(emoji)
            .font(.system(size: 100))
        }
      }
      .padding()
    }
  }
}

CustomAnimation

You need to adopt the AnimatableStyle protocol.

public protocol AnimatableStyle {
  var opacity: AnimationValue<CGFloat> { get }
  var offsetX: AnimationValue<CGFloat> { get }
  var offsetY: AnimationValue<CGFloat> { get }
  var rotation: AnimationValue<Double> { get }
  var scale: AnimationValue<Double> { get }
  var blur: AnimationValue<Double> { get }
  var delay: Double { get }
  var animation: Animation { get }
  var maxAnimationCount: Int { get }
}

You can create a struct that adopts the protocol as shown below.

struct YourCustomAnimationConfig: AnimatableStyle {
  var opacity: AnimationValue<CGFloat> = .init(from: 0, to: 1)
  var offsetX: AnimationValue<CGFloat> = .init(from: 0, to: 0)
  var offsetY: AnimationValue<CGFloat> = .init(from: 50, to: 0)
  var rotation: AnimationValue<Double> = .init(from: 0, to: 0)
  var scale: AnimationValue<Double> = .init(from: 1, to: 1)
  var blur: AnimationValue<Double> = .init(from: 8, to: 0)
  var delay: Double = 0.05
  var animation: Animation = .bouncy(duration: 0.4)
  var maxAnimationCount: Int = 20
}
AnimatedForEach(emojiData, preset: YourCustomAnimationConfig()) { emoji in

}

You can modify the values of AnimationConfig as needed.

public struct AnimationConfig: Hashable, Equatable, AnimatableStyle {
  public var opacity: AnimationValue<CGFloat>
  public var offsetX: AnimationValue<CGFloat>
  public var offsetY: AnimationValue<CGFloat>
  public var rotation: AnimationValue<Double>
  public var scale: AnimationValue<Double>
  public var blur: AnimationValue<Double>
  public var delay: Double
  public var animation: Animation
  public var maxAnimationCount: Int

  public init(
    opacity: AnimationValue<CGFloat>,
    offsetX: AnimationValue<CGFloat>,
    offsetY: AnimationValue<CGFloat>,
    rotation: AnimationValue<Double>,
    scale: AnimationValue<Double>,
    blur: AnimationValue<Double>,
    delay: Double = 0.05,
    animation: Animation = .bouncy(duration: 0.4),
    maxAnimationCount: Int = 20
  ) {
    self.opacity = opacity
    self.offsetX = offsetX
    self.offsetY = offsetY
    self.rotation = rotation
    self.scale = scale
    self.blur = blur
    self.delay = delay
    self.animation = animation
    self.maxAnimationCount = maxAnimationCount
  }
}
let preset: AnimationConfig = AnimationConfig(
  opacity: .init(from: 0, to: 1),
  offsetX: .init(from: 50, to: 0),
  offsetY: .init(from: 10, to: 0),
  rotation: .init(from: 0, to: 0),
  scale: .init(from: 1.3, to: 1),
  blur: .init(from: 8, to: 0)
)

AnimatedForEach(emojiData, preset: preset) { emoji in

}

Explanation of AnimatableStyle Variables

Variable Type Description
opacity AnimationValue<CGFloat> Defines the animation values for opacity.
offsetX AnimationValue<CGFloat> Defines the animation values for horizontal offset.
offsetY AnimationValue<CGFloat> Defines the animation values for vertical offset.
rotation AnimationValue<Double> Defines the animation values for rotation.
scale AnimationValue<Double> Defines the animation values for scaling.
blur AnimationValue<Double> Defines the animation values for blur effect.
delay Double Determines the delay before each animation starts.
animation Animation Set to your desired animation type.
maxAnimationCount Int Maximum number of animations to play.
If set to 20, animations play up to the 20th item (index 19) according to the delay.
The 21st and subsequent items will animate simultaneously with the 21st.
This prevents excessive delays when animating large sets of data.
For example, without this option, you would need to wait delay * 99 seconds to see the 100th item.

AnimationStyle Preset

var delay: Double = 0.05,
var animation: Animation = .bouncy(duration: 0.4),
var maxAnimationCount: Int = 20

extension AnimationStyle {
  var config: AnimationConfig {
    switch self {
    case .`default`:
      return AnimationConfig(
        opacity: .init(from: 0, to: 1),
        offsetX: .init(from: 0, to: 0),
        offsetY: .init(from: 50, to: 0),
        rotation: .init(from: 0, to: 0),
        scale: .init(from: 1.0, to: 1.0),
        blur: .init(from: 0, to: 0)
      )
    case .grid:
      return AnimationConfig(
        opacity: .init(from: 0, to: 1),
        offsetX: .init(from: 0, to: 0),
        offsetY: .init(from: 100, to: 0),
        rotation: .init(from: 0, to: 0),
        scale: .init(from: 1.3, to: 1.0),
        blur: .init(from: 8, to: 0)
      )
    case .list:
      return AnimationConfig(
        opacity: .init(from: 0, to: 1),
        offsetX: .init(from: 50, to: 0),
        offsetY: .init(from: 0, to: 0),
        rotation: .init(from: 0, to: 0),
        scale: .init(from: 1.1, to: 1.0),
        blur: .init(from: 0, to: 0)
      )
    }
  }
}