First, you don't need to include both the starting point and the ending point in the same struct. The point of Animatable is to put both endpoints in two instances, and interpolated between them.
struct ForegroundColor: AnimatableModifier {
private var red, green, blue, alpha: Double
init(color: Color) { ... }
func body(content: Content) -> some View { ... }
var animatableData: AnimatablePair<AnimatablePair<Double, Double>, AnimatablePair<Double, Double>> {
get { .init(.init(red, green), .init(blue, alpha)) }
set {
red = newValue.first.first
green = newValue.first.second
blue = newValue.second.first
alpha = newValue.second.second
}
}
}
So that you can use it like this:
Text("Test")
.modifier(ForegroundColor(color: flag ? .green : .blue))
which is much more flexible.
Now, where am I? Yes, conversion. In this scenario, the UIColor vents out CGFloat, but the Color takes in Double, so conversion is required. You can use Double and perform conversion inside init. You can also use CGFloat and do the conversion inside body. Recall that body is called at every frame, so it's probably better to push the work toward init.
I did say before to be aware of the colour space because CGColor.components returns the colour components w.r.t. its colour space. If your colour is grayscale, it'd have only two components, crashing the system when you try to access components[2].
It's probably fine with Color. Though I'd do
init(color: Color) {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
guard UIColor(color).getRed(&r, green: &g, blue: &b, alpha: &a) else {
(red, green, blue, alpha) = (0, 0, 0, 1)
logger.error(...)
return
}
red = Double(r)
green = Double(g)
blue = Double(b)
alpha = Double(a)
}