I am trying to create snake like view border animation with gradient colour, see the image below.
I have tried this UIKit solution, but CAGradientLayer with CAKeyframeAnimation is not working. It is coming as background view not as a border.
Also I tried with SwiftUI, but the gradient animation is breaking for some dynamic height and width. Below is the SwiftUI code I have used:
struct GradientBorderAnimationView: View {
@State var rotation:CGFloat = 0.0
@State var width: CGFloat = 330
@State var height: CGFloat = 100
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 20, style: .continuous)
.frame(width: width*2, height: height*2)
.foregroundStyle(LinearGradient(gradient: Gradient(colors: [.white, .white, .blue]),
startPoint: .top,
endPoint: .bottom))
.rotationEffect(.degrees(rotation))
.mask {
RoundedRectangle(cornerRadius: 20, style: .continuous)
.stroke(lineWidth: 3)
.frame(width: width, height: height)
}
}
.ignoresSafeArea()
.onAppear{
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)){
rotation = 360
}
}
}
}
Output:

Expected Output:

Can anyone tell me what is the issue in the above code or if anyone has any better solution, that also works. I am okay with SwiftUI and UIKit anything.
As suggested in another answer, it works better to use an AngularGradient. Also, instead of applying rotation to an underlying shape and then using a mask to show an outline, you can use .stroke to stroke the gradient at a changing angle.
However, the difficult bit is making it look like the snake is moving at a constant speed around the frame. Since the effect is based on an angle, the angle needs to change faster when the head of the snake is at the corners. One way to achieve this is to use an Animatable ViewModifier.
A ViewModifier can only be used to apply regular modifiers to a view. For this animation, it is the angle of the gradient that needs modifying. So as a way to implement, the ViewModifer can apply the gradient as an overlay.
The position for the end of the snake can be determined from the progress fraction by trimming the path and then obtaining the point at the end of the path using Path.currentPoint.
The point at the end of the path can be converted to an angle based on the center-point of the rectangle.
The color stops also need to depend on the current angle, so that the stops maintain the same relative distance to each other and the length of the snake stays the same as it goes round the corners.
The positions for the stops can be computed using the same technique of trimming the path and determining the end point.
Here is how it all comes together:
struct SnakeBorder: ViewModifier, Animatable {
var animatableData: CGFloat
private let path: Path
private let center: CGPoint
init(w: CGFloat, h: CGFloat, cornerRadius: CGFloat, fraction: CGFloat) {
self.animatableData = fraction
self.path = Path(
roundedRect: CGRect(x: 0, y: 0, width: w, height: h),
cornerRadius: cornerRadius
)
self.center = CGPoint(x: w / 2, y: h / 2)
}
private func radiansForFraction(fraction: CGFloat) -> CGFloat {
if let position = path.trimmedPath(from: 0, to: fraction).currentPoint {
atan2(position.y - center.y, position.x - center.x)
} else {
0
}
}
private func adjustedFraction(fraction: CGFloat, currentRadians: CGFloat) -> CGFloat {
let combinedFraction = animatableData + fraction
let normalizedFraction = combinedFraction > 1 ? combinedFraction - 1 : combinedFraction
let dRadians = radiansForFraction(fraction: normalizedFraction) - currentRadians
let computedFraction = dRadians / (2 * .pi)
return computedFraction < 0 ? computedFraction + 1 : computedFraction
}
private func stops(currentRadians: CGFloat) -> [Gradient.Stop] {
[
.init(color: .white, location: 0),
.init(color: .blue, location: adjustedFraction(fraction: 0.1, currentRadians: currentRadians)),
.init(color: .blue, location: adjustedFraction(fraction: 0.3, currentRadians: currentRadians)),
.init(color: .white, location: adjustedFraction(fraction: 0.4, currentRadians: currentRadians))
]
}
func body(content: Content) -> some View {
let currentRadians = radiansForFraction(fraction: animatableData)
content
.overlay {
path
.stroke(
AngularGradient(
stops: stops(currentRadians: currentRadians),
center: .center,
angle: .radians(currentRadians)
),
lineWidth: 5
)
}
}
}
struct GradientBorderAnimationView: View {
let width: CGFloat = 330
let height: CGFloat = 100
@State var fraction: CGFloat = 0
var body: some View {
Color.clear
.frame(width: width, height: height)
.modifier(SnakeBorder(w: width, h: height, cornerRadius: 20, fraction: fraction))
.onAppear{
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)){
fraction = 1
}
}
}
}

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With