I'm animating a line horizontally and it works fine, except that the effect I'm aiming for is to have a slight springy bounce before it alternates sides. I thought the .easeInOut
effect would emulate that but it doesn't feel springy. How can I combine the .spring
animation such that the line sort of bounces for a fraction of a second at each end before it moves to the alternate side?
struct Line: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.size.height / 2))
path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height / 2))
return path
}
}
struct LineView: View {
let height: CGFloat
@State private var animating = false
var body: some View {
GeometryReader { geometry in
let lineWidth = geometry.size.width / 3
Line()
.stroke(Color.black, lineWidth: height)
.frame(width: lineWidth)
.offset(x: animating ? 0 : geometry.size.width - lineWidth)
.animation(.easeInOut(duration: 1.0).repeatForever(), value: animating)
.onAppear {
animating.toggle()
}
}
}
}
struct ContentView: View {
var body: some View {
VStack {
LineView(height: 5.0)
.frame(width: 200)
}
}
}
Any help is appreciated.
The intended effect (blue line): https://streamable.com/ijwe1w
You are on the right track, but you are over engineering your solution. Essentially, you need to make 3 layers. The first is a background as perceived by the user, but it is really the base view. On top of that, you add your visual. For simplicity, I used capsules. The top layer is a mask which is where the magic happens.
To achieve the look you desire, the middle layer should overshoot the bottom layer in the offset, by how much you want it to "shrink". You then put a mask on top that is the same size as the bottom layer to act as a window that only allows the user to see that much of what is happening below. Here is what I believe you are looking for:
struct LineView: View {
let height: CGFloat
@State private var animating = false
var body: some View {
GeometryReader { geometry in
Capsule() // Base layer
.fill(.gray.opacity(0.3))
.frame(height: height + 2)
.overlay(alignment: .leading) {
let lineWidth = geometry.size.width / 3
Capsule()
.fill(.blue)
.frame(width: lineWidth)
// Here I am overshooting by half the line width in both directions
.offset(x: animating ? -(lineWidth / 2) : geometry.size.width - lineWidth / 2)
.animation(.easeInOut(duration: 1.0).repeatForever(), value: animating)
}
// Then mask over with a FillStyle(eoFill: true)
.mask(
Capsule()
.fill(style: FillStyle(eoFill: true))
.frame(height: height + 2)
)
.onAppear {
animating.toggle()
}
.frame(maxHeight: .infinity)
}
}
}
In the end, you get this:
(Note, this is a gif, and not as smooth as the actual animation)