'GradientAnimation ONLY Works in AnimatableModifier, Other Simultaneous Animations Working Regardless (SwiftUI)

I'm having a lot of trouble narrowing down the underlying implementation that might cause things to work this way. I've tried tons of combinations of using @State vs initializing, overlays, whatever.

1. The only thing that works is when it is in an AnimatableModifier.

2. Other animations on the same view, and based on same value work regardless.

Visual Example

You can see the scale animation working fine, but only the bottom rectangle is animating the gradient

gradient-issue

Reproducible Code

I got some of the original code here. They mention weird issues with containers, but it's not clear what is expected behavior and otherwise

Works with Previews

NOTE: if you're using previews, make sure to play or animations won't run at all

import SwiftUI
import UIKit

public struct AnimatableGradient: View, Animatable {
    public let from: [Color]
    public let to: [Color]
    public var pct: CGFloat
    
    public var animatableData: CGFloat {
        get { pct }
        set { pct = newValue }
    }

    private var current: [Color] {
        zip(from, to).map { from, to in
            from.mix(with: to, percent: pct)
        }
    }

    public var body: some View {
        LinearGradient(
            gradient: Gradient(colors: current),
            startPoint: UnitPoint(x: 0, y: 0),
            endPoint: UnitPoint(x: 1, y: 1)
        )
        /// temporary to show that animating is working
        .scaleEffect(1 - (0.2 * pct))
    }
}

// MARK: AnimatableModifier

struct PassthroughModifier<Body: View>: AnimatableModifier {
    var animatableData: CGFloat
    let body: (CGFloat) -> Body
    
    func body(content: Content) -> some View {
        // also works to just pass body(animatableData)
        content.overlay(body(animatableData))
    }
}

// MARK: Content

fileprivate struct GradientExample: View, Animatable {
    @State var pct: CGFloat = 0
    
    private var base: some View {
        Rectangle()
            .fill(.black)
            .frame(width: 200, height: 100)
    }
    
    private func gradient(pct: CGFloat) -> some View {
        AnimatableGradient(from: [.orange, .red],
                           to: [.blue, .green],
                           pct: pct)
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            base.overlay(
                gradient(pct: pct)
            )

            base.modifier(
                PassthroughModifier(animatableData: pct){ pct in
                    gradient(pct: pct)
                }
            )
        }
        .onAppear{
            withAnimation(.linear(duration: 2.8).repeatForever(autoreverses: true)) {
                self.pct = pct == 0 ? 1 : 0
            }
        }
    }
}

// MARK: Preview

struct GradientPreview: PreviewProvider {
    static var previews: some View {
        GradientExample()
    }
}

// MARK: Helpers

extension Color {
    public var components: (r: Double, g: Double, b: Double, a: Double) {
        /// passing through UIColor because things like `Color.red`
        /// always return `nil` otherwise :/
        let comps = UIColor(self).cgColor.components ?? []
        return (
            comps[safe: 0] ?? 0,
            comps[safe: 1] ?? 0,
            comps[safe: 2] ?? 0,
            comps[safe: 3] ?? 0
        )
    }
}

extension Array {
    subscript(safe idx: Int) -> Element? {
        guard idx < count else { return nil }
        return self[idx]
    }
}

extension Color {
    public func mix(with other: Color, percent: Double) -> Color {
        let left = self.components
        let right = other.components
        let r = left.r + right.r - (left.r * percent)
        let g = left.g + right.g - (left.g * percent)
        let b = left.b + right.b - (left.b * percent)
        
        return Color(red: Double(r), green: Double(g), blue: Double(b))
    }
}


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source