Home > Software engineering >  How can I animate my capsule's progress when a button is clicked?
How can I animate my capsule's progress when a button is clicked?

Time:04-19

I hope you are all doing good. I'm very new to this community and I hope we can help each other grow.

I'm currently building a MacOS App with swiftUI and I have this capsule shape, that has a water animation inside. The water animation are just waves that move horizontally created with animatable data and path. My problem is, I have two other buttons on the screen, a play button and a stop button. They are supposed to start filling the capsule with the water and stop doing it respectively, which they do, but they are supposed to do it with an animation, and it's not.

Below is my code for further details. Thanks in advance.

GeometryReader { geometry in
                                
    VStack {

        Spacer()
        
        BreathingWave(progress: $progressValue, phase: phase)
            .fill(Color(#colorLiteral(red: 0.14848575, green: 0.6356075406, blue: 0.5744615197, alpha: 1)))
            .clipShape(Capsule())
            .border(Color.gray, width: 3)
            .frame(width: geometry.size.width / 12)
            .onAppear {
                withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
                    self.phase = .pi * 2
                }
            }
        
        HStack {

            Spacer()

            Image(systemName: "play.circle.fill")
                .resizable()
                .renderingMode(.template)
                .frame(width: 50, height: 50)
                .foregroundColor(Color(#colorLiteral(red: 0.14848575, green: 0.6356075406, blue: 0.5744615197, alpha: 1)))
                .scaleEffect(scalePlay ? 1.25 : 1)
                .onHover(perform: { scalPlay in
                    withAnimation(.linear(duration: 0.1)) {
                        scalePlay = scalPlay
                    }
                })
                .onTapGesture {
                    withAnimation(
                        .easeInOut(duration: duration)
                        .repeatForever(autoreverses: true)) {
                            if(progressValue < 1) {
                                progressValue  = 0.1
                            }
                            else {
                                progressValue = progressValue
                            }
                        }
                }

            Spacer()

            Image(systemName: "stop.circle.fill")
                .resizable()
                .renderingMode(.template)
                .frame(width: 50, height: 50)
                .foregroundColor(Color(#colorLiteral(red: 0.14848575, green: 0.6356075406, blue: 0.5744615197, alpha: 1)))
                .scaleEffect(scaleStop ? 1.25 : 1)
                .onHover(perform: { scalStop in
                    withAnimation(.linear(duration: 0.1)) {
                        scaleStop = scalStop
                    }
                })
                .onTapGesture {
                    withAnimation(.linear) {
                        progressValue = 0.0
                    }
                }

            Spacer()

        }
        .padding(.bottom, 50)
    }
}

And this is the code of the BreathingWave

struct BreathingWave: Shape {
    
    @Binding var progress: Float
    var applitude: CGFloat = 10
    var waveLength: CGFloat = 20
    var phase: CGFloat

    var animatableData: CGFloat {
        get { phase }
        set { phase = newValue }
    }
    
    func path(in rect: CGRect) -> Path {
        
        var path = Path()
        
        let width = rect.width
        let height = rect.height
        let minWidth = width / 2
        let progressHeight = height * (1 - CGFloat(progress))
        
        path.move(to: CGPoint(x: 0, y: progressHeight))
        
        for x in stride(from: 0, to: width   5, by: 5) {
            let relativeX = x / waveLength
            let normalizedLength = (x - minWidth) / minWidth
            let y = progressHeight   sin(phase   relativeX) * applitude * normalizedLength
            path.addLine(to: CGPoint(x: x, y: y))
        }
        path.addLine(to: CGPoint(x: width, y: progressHeight))
        path.addLine(to: CGPoint(x: width, y: height))
        path.addLine(to: CGPoint(x: 0, y: height))
        path.addLine(to: CGPoint(x: 0, y: progressHeight))
        
        return path
    }
}

CodePudding user response:

The problem you are running into is simply that you never told BreathingWave how to animate for progress. You set a single dimension animation, when you really want it to animate in two dimensions. The fix is straightforward: use AnimatablePair to supply the variables to animatableData.

struct BreathingWave: Shape {
    
    var applitude: CGFloat = 10
    var waveLength: CGFloat = 20
    // progress is no longer a Binding. Using @Binding never did anything
    // as you were not changing the value to be used in the parent view.
    var progress: Float
    var phase: CGFloat

    var animatableData: AnimatablePair<Float, CGFloat> { // <- AnimatablePair here
        get { AnimatablePair(progress, phase) } // <- the getter returns the pair
        set { progress = newValue.first // <- the setter sets each individually from
            phase = newValue.second     //    the pair.
        }
    }
    
    func path(in rect: CGRect) -> Path {
        ...
    }
}

Lastly, you now call it like this:

BreathingWave(progress: progressValue, phase: phase)

One final thing. Please provide a Minimal Reproducible Example (MRE) in the future. There was a lot of code that was unnecessary to debug this, and I had to implementment the struct header to run it. Strip as much out as possible, but give us something that we can copy, paste and run.

  • Related