Search code examples
animationswiftuiloading

How to create an indefinite gradient loading like animated view?


I would like to create a view with a gradient and an infinite loading like animation (the darkest part of the gradient would start from the left of the view, then take 2 seconds to animate to the right, and as it "exits" the view on the right, it would appear again on its left).

A visual example (not animated, sorry) :

enter image description here

Here is what I started writing but it doesn't work as I want.

struct TestGradientView: View {
    @State private var animateGradient = false
    
    var body: some View {
        LinearGradient(
            colors: [
                .gray.opacity(0.25),
                .gray.opacity(0.5)
            ],
            startPoint: animateGradient ? .leading : .trailing,
            endPoint: !animateGradient ? .leading : .trailing
        )
        .onAppear {
            withAnimation(
                .linear(duration: 2.0)
                .repeatForever(autoreverses: true)
            ) {
                animateGradient.toggle()
            }
        }
    }
}

Use:

VStack {
    Spacer()
    TestGradientView()
        .cornerRadius(4.0)
        .padding(.horizontal, 16)
        .frame(height: 8)
    Spacer()
}

Solution

  • Animation needs some fractional data, so just binary switch actually does not work - it is just a toggle.

    A possible approach is to construct positional gradient and let animation know that some of color position in gradient is changed, so animatable. This can be done with AnimatableModifier.

    Here is a demo (Xcode 13.4 / iOS 15.5)

    enter image description here

    Main part:

        RoundedRectangle(cornerRadius: 12).fill(.clear)
            .modifier(GradientProgressEffect(position: animate))
            .animation(.linear(duration: 2.0)
                .repeatForever(autoreverses: true), value: animate)
            .onAppear {
                animate = 0.9
            }
    
     // ...
    
        LinearGradient(
            stops: [
                .init(color: .gray.opacity(0.1), location: 0.0),
                .init(color: .gray.opacity(1), location: position - 0.05),
                .init(color: .gray.opacity(1), location: position + 0.05),
                .init(color: .gray.opacity(0.1), location: 1.0),
            ],
            startPoint: .leading,
            endPoint: .trailing
        )
    

    *constants can be fit per needs

    Complete test module is here