skip to Main Content

I have a WaveView which is just a sine wave and a rectangle. To make it act like a real wave, I need it to translate unstoppably. So I wrote a Wave view and added an animation on offset with a timer in .onAppear. (I’ve already tried .animation(.linear(duration: 3).repeatForever(autoreverses: false), value: offset) but that has the same issue)
Then, as soon as the Wave view appears, it keeps moving like a wave. Everything works fine.

Then I want to add another animation to the variable progress so that I can animate the progress change as well.

What I expect is that the wave keeps moving and the progress (the height of the blue part that we can see) goes up with an animation.
But as soon as the progress changes, the animation on progress gets performed but the wave animation stops, until the timer fires for the next time.

What should I do to keep the wave animation while animating progress?

p.s. Things gets worse with .animation(.linear(duration: 3).repeatForever(autoreverses: false), value: offset) since there’s no timer, and I only change offset once, so once this animation gets interrupted, the wave just never start moving again.

struct WaveView: View {
    
    var waveHeight: CGFloat
    
    var body: some View {
        GeometryReader { global in
            Path { path in
                let width = global.size.width
                
                path.move(to: CGPoint(x: width*2.0, y: waveHeight))
                path.addLine(to: CGPoint(x:width*2.0,y: global.size.height))
                path.addLine(to: CGPoint(x:0,y:global.size.height))
                path.addLine(to: CGPoint(x:0,y: waveHeight))
                
                var points = [CGPoint]()
                for angle in stride(from: 0, through: 180.0*4, by: 1) {
                    let radian = angle * .pi / 180
                    let cosValue = cos(radian)
                    let x =  CGFloat(angle) * global.size.width / 360
                    let y = (1+cosValue) * waveHeight / 2
                    points.append(CGPoint(x: x, y: y))
                }
                
                path.addLines(points)
            }
            .fill(.blue.opacity(0.5))
        }
    }
}

struct Wave: View {
    
    init(progress: Binding<Float>, waveHeight height: CGFloat = 200) {
        waveHeight = height
        self._progress = progress
    }
    
    var waveHeight: CGFloat
    
    @Binding var progress: Float
    @State var offset: CGFloat = 0
    
    
    @State var timer: Timer?
    
    var body: some View {
        GeometryReader { global in
            ZStack(alignment: .bottom) {
                    WaveView(waveHeight: waveHeight)
                    .onAppear {
                        timer = .scheduledTimer(withTimeInterval: 3, repeats: true, block: { _ in
                            if offset >= 1 {
                                offset = 0
                            }
                            withAnimation(.linear(duration: 3)) {
                                offset += 1
                            }
                        })
                        timer?.fire()
                    }
                    .onDisappear {
                        timer?.invalidate()
                        timer = nil
                    }
            }
            .frame(width: global.size.width, height: global.size.height)
            
            .position(x: global.size.width * (0.5-offset),y: global.size.height/2)
            .offset(y:CGFloat(1-progress)*global.size.height)
            .animation(.easeInOut(duration: 0.4), value: progress)
            
        }
    }
}

struct TestView: View {
    
    @State var progress: Float = 0.6
    
    var body: some View {
        ZStack{
            Wave(progress: $progress, waveHeight: 35)
                .edgesIgnoringSafeArea(.all)
            VStack {
                Stepper("progress: (progress.description)",value: $progress, in: 0.0...1.0, step: 0.2)
                    .padding(.horizontal,40)
            }
        }
    }
}

#Preview {
    TestView()
}

normal wave
normal wave

wave animation that gets interrupted by animation on progress
wave animation that gets interrupted by animation on progress

2

Answers


  1. Chosen as BEST ANSWER

    One solution is to use .drawingGroup(), thanks to Benzy Neez.

    One thing to mention is that .drawingGroup() also works without using VStack. It seems that it can be used to insulate any two animations to make them act together.

    Here's a revised version:

    struct Wave: View {
        
        init(progress: Binding<Float>, waveHeight height: CGFloat = 200) {
            waveHeight = height
            self._progress = progress
        }
        
        var waveHeight: CGFloat
        
        @Binding var progress: Float
        @State var offset: CGFloat = 0
        
        var body: some View {
            GeometryReader { global in
                ZStack(alignment: .bottom) {
                    WaveView(waveHeight: waveHeight)
                        .onAppear {
                            if offset >= 1 {
                                offset = 0
                            }
                            offset += 1
                        }
                }
                .frame(width: global.size.width, height: global.size.height)
                // Animation 1
                .position(x: global.size.width * (0.5-offset),y: global.size.height/2)
                .animation(.linear(duration: 3).repeatForever(autoreverses: false), value: offset)
                .drawingGroup() // <- THE KEY TO INSULATE TWO ANIMATIONS
                // Animation 2
                .offset(y:CGFloat(1-progress)*global.size.height)
                .animation(.easeInOut(duration: 0.4), value: progress)
                
            }
        }
    }
    

    In the code above, .drawingGroup() after Animation1 insulates Animation1 and the animations below. The result is that Animation2 will not interrupt Animation1 anymore.

    I couldn't find a lot of documents discussing the effect of insulation, but I guess it worked because .drawingGroup() flattened the view into one layer so that the frames as the result of the animation remained but animation data get removed so that SwiftUI thinks there's no animation right after .drawingGroup().


  2. As I was suggesting in a comment, I thought it might help to detach the wave animation from the progress animation, so that they can work independently. However, it turns out it’s not as simple as that.

    I was able to reproduce the problem using a simplified version of your animation which just has a ball moving from side to side. The ball animation is a substitute for your wave animation, which is probably not so trivial.

    • If the animated ball is left running in exactly the same position, it continues to run, even when other animations are happening on the screen.
    • However, I found that as soon as the animated ball is impacted by another animated change, such as if the vertical position is changed, it would stop. This is also the problem you were describing.

    I found the key to getting it working is to add .drawingGroup() to the continuous animation. This seems to insulate it from other changes on the screen.

    Here is the simplified example that shows it working:

    struct Ball: View {
    
        let size: CGFloat
        @State private var xOffset = CGFloat.zero
    
        var body: some View {
            GeometryReader { proxy in
                Circle()
                    .fill(.red)
                    .frame(width: size, height: size)
                    .offset(x: xOffset)
                    .onAppear { xOffset = proxy.size.width - size }
                    .animation(
                        .easeInOut(duration: 2).repeatForever(autoreverses: true),
                        value: xOffset
                    )
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .frame(height: size)
        }
    }
    
    struct TestView: View {
    
        @State private var progress: CGFloat = 0.6
    
        var body: some View {
            VStack {
                Stepper("progress: (String(format: "%.2f", progress))", value: $progress, in: 0.0...1.0, step: 0.2)
                    .padding(.horizontal,40)
                Spacer()
                Ball(size: 35)
                    .drawingGroup() // <- THIS IS KEY
                Color.blue
                    .opacity(0.5)
                    .frame(height: progress * 300)
            }
            .animation(.easeInOut(duration: 0.4), value: progress)
            .frame(height: 400)
        }
    }
    

    Animation

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search