8000 GitHub - dankinsoid/VDAnimation: Declarative way to create animations
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

dankinsoid/VDAnimation

Repository files navigation

VDAnimation

Declarative Animations for SwiftUI

VDAnimation provides a powerful, declarative way to create complex animations in Swift with minimal code. Compose animations sequentially, in parallel, with custom timing and curves.

Features

  • 🎭 Declarative animation composition
  • 🔄 Sequence and parallel animations
  • 🎚 Interactive animation control
  • 🎯 Side effects
  • 🏗 Built-in support for custom value interpolation
  • 📱 Working on iOS 13 and macOS 10.15 or later
  • đź§© SwiftUI and UIKit support

Examples

Animating Complex Types

struct LoaderAnimation: View {

    @MotionState private var state = Tween(0.0, 0.01)
    private let arcSize = 0.4

    var body: some View {
        WithMotion(_state) { value in
            Circle()
                .trim(from: value.start, to: value.end)
                .stroke(
                    Color.white,
                    style: StrokeStyle(lineWidth: 8, lineCap: .round)
                )
                .frame(width: 50, height: 50)
                .rotationEffect(-.degrees(90), anchor: .center)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.green)
        } motion: {
            Sequential {
                Parallel()
                    .end(arcSize) // animate .end property of the state
                    .curve(.cubicEaseIn)
    
                To(Tween(1 - arcSize, 1.0)) // animate the whole state
                    .duration(.relative((1 - arcSize) / (1 + arcSize))) // compute duration to keep movement speed constant
    
                Parallel()
                    .start(1.0 - 0.01) // animate .start property of the state
                    .curve(.cubicEaseOut)
            }
            .duration(1)
            .sync() // synchronize all loaders across the app
        }
        .onAppear {
            $state.play(repeat: true)
        }
    }
}

Animating Collections

struct DotsAnimation: View {

    @MotionState private var values: [CGFloat] = [0, 0, 0]

    var body: some View {
        WithMotion(_values) { values in
            HStack(spacing: 12) {
                ForEach(Array(values.enumerated()), id: \.offset) { value in
                    Circle()
                        .fill(.white)
                        .frame(width: 12, height: 12)
                        .offset(y: value.element)
                }
            }
        } motion: {
            Parallel { index in
                To(-10)
                    .duration(0.3)
                    .curve(.easeInOut)
                    .autoreverse()
                    .delay(.relative(Double(index) / Double(values.count * 2 - 1)))
            }
            .sync() // synchronize all loaders across the app
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.blue)
        .onAppear {
            $values.play(repeat: true)
        }
    }
}

Animating Paths Morphing

struct PathAnimation: View {
    @MotionState var path: Path = Self.heartPath

    var body: some View {
        VStack {
            WithMotion(_path) { path in
                path.fill()
            } motion: {
                Sequential {
                    Wait()
                    To(Self.dropPath)
                    Wait()
                    To(Self.starPath)
                    Wait()
                    To(Self.heartPath)
                }
                .duration(2)
            }
            .frame(width: 100, height: 100)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.purple)
            .foregroundColor(.white)
        }
        .onAppear {
            $path.play(repeat: true)
        }
    }
}

Interactive Animation Control

struct InteractiveAnimation: View {

    @MotionState private var animation = Props()
    
    @Tweenable
    struct Props {
        var color: Color = .red
        var angle: Angle = .zero
        var offset: CGFloat = -120
    }
    
    var body: some View {
        VStack(spacing: 0) {
            WithMotion(_animation) { props in
                VStack(spacing: 10) {
                    Rectangle()
                        .fill(props.color)
                        .rotationEffect(props.angle, anchor: .center)
                        .offset(x: props.offset)
                        .frame(width: 100, height: 100)
                    Slider(value: _animation.$progress, in: 0...1)
                        .padding(.horizontal)
                }
            } motion: {
                To(
                    Props(
                        color: .blue,
                        angle: .degrees(360),
                        offset: 120
                    )
                )
                .duration(2.0)
            }
            
            if $animation.isAnimating {
                Button("Pause") { $animation.pause() }
            } else {
                Button("Play") {
                    if $animation.progress == 1.0 || $animation.progress == 0.0 {
                        $animation.reverse()
                    } else {
                        $animation.play()
                    }
                }
            }
        }
    }
}

Complex movement

struct ComplexMovement: View {

    @MotionState var location = CGPoint(x: -100, y: 0)

    var body: some View {
        Circle()
            .fill(Color.white)
            .withMotion(_location) {
                $0.position($1)
            } motion: {
                Lerp { t in
                    CGPoint(
                        x: cos(Double.lerp(0, .pi * 2, t)) * 100,
                        y: sin(Double.lerp(0, .pi * 6, t)) * 40
                    )
                }
                .duration(2)
            }
            .offset(y: 10)
            .frame(width: 40, height: 40)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.red)
            .onTapGesture {
                if $location.isAnimating {
                    $location.stop()
                } else {
                    $location.play(repeat: true)
                }
            }
    }
}

UIKit CADisplayLink wrapper

motionDisplayLink(Value(amount: 0, color: .systemRed)) { [label] value in
    label.text = "\(value.amount) USD"
    label.textColor = value.color
} motion: {
    To(Value(amount: 1000, color: .systemGreen))
        .delay(.relative(0.2))
        .delayAfter(.relative(0.2))
        .duration(2)
}
.play()

Usage

Installation

Swift Package Manager

Add the package dependency to your Package.swift:

dependencies: [
    .package(url: "https://github.com/dankinsoid/VDAnimation.git", from: "2.3.0")
]

Or add it directly in Xcode via File > Add Packages...

License

VDAnimation is available under the MIT license. See the LICENSE file for more info.

About

Declarative way to create animations

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published
0