Konrad
Konrad

Reputation: 18585

Restarting perpetual animation after stopping in SwiftUI

Background

In this learning app, I've followed and excellent tutorial from Hacking with Swift on generating a wave-like animation. I've modified this app further adding some functionalities:

Problem

After stopping the animation does not "run" again. This is demonstrated in the gif below.

Animation Preview

After stopping the animation does not restart.

Code

//
//  ContentView.swift
//  WaveExample
//
//  Created by Konrad on 28/07/2021.
//  Original tutorial: https://www.hackingwithswift.com/plus/custom-swiftui-components/creating-a-waveview-to-draw-smooth-waveforms
//

import SwiftUI

/**
 Creates wave shape object
 - Parameter strength: How tall the wave should be
 - Parameter frequency: How densly the wave should be packed
 - returns: Shape
 */
struct Wave: Shape {
    // Basic wave characteristics
    var strength: Double    // Height
    var frequency: Double   // Number of hills
    var phase: Double       // Offsets the wave, can be used to animate the view

    // Required to define that animation relates to moving the wave from left to right
    var animatableData: Double {
        get { phase }
        set { self.phase = newValue }
    }

    // Path drawing function
    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath()

        // Basic waveline characteristics
        let width = Double(rect.width)
        let height = Double(rect.height)
        let midWidth = width / 2
        let midHeight = height / 2
        let wavelength = width / frequency
        let oneOverMidWidth = 1 / midWidth

        // Path characteristics
        path.move(to: CGPoint(x: 0, y: midHeight))

        // By determines the nmber of calculations, can be decreased to run faster
        for xPosition in stride(from: 0, through: width, by: 1) {
            let relativeX = xPosition / wavelength          // How far we are from the start point
            let distanceFromMidWidth = xPosition - midWidth // Distance from the middle of the space
            let normalDistance = distanceFromMidWidth * oneOverMidWidth // Get values from -1 to 1, normalize
            // let parabola = normalDistance // Small waves in the middle
            let parabola = -(normalDistance * normalDistance) + 1 // Big wave in the middle
            let sine = sin(relativeX + phase)       // Offset based on phase
            let yPosition = parabola * strength * sine + midHeight     // Moving this halfway
            path.addLine(to: CGPoint(x: xPosition, y: yPosition))
        }

        return Path(path.cgPath)
    }
}

struct Line: Shape {
    func path(in rect: CGRect) -> Path {

        // Positioning
        let midHeight = rect.height / 2

        let path = UIBezierPath()
        path.move(to: CGPoint(x: 0, y: midHeight))
        path.addLine(to: CGPoint(x: rect.width, y: midHeight))
        return Path(path.cgPath)
    }
}

struct ContentView: View {

    @State private var phase = 0.0                     // Used to animate the wave
    @State private var waveStrength: Double = 10.0     // How tall, change for interesting numbers
    @State private var waveFrequency: Double = 10.0    // How frequent, change for interesting numbers

    @State var isAnimating: Bool = false    // Currently running animation
    @State private var randNum: Int16 = 0   // Random number to keep generating while animating
    @State private var isNumberInteresting: Bool = false // Will take 'true' of the random number has some interesting properties

    // Timer publisher reflecting frequent animation changes
    @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    // Stop timer
    func stopTimer() {
        self.timer.upstream.connect().cancel()
    }
    // Start timer
    func startTimer() {
        self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    }

    // Check if number is interesting
    func checkNumber(num: Int16) -> Bool {
        var isInteresting: Bool = false
        if num % 2 == 0 {
            isInteresting.toggle()
        }
        return isInteresting
    }

    var body: some View {
        VStack {
            if self.isAnimating {
                VStack {
                    Button("Stop") {
                        self.isAnimating = false
                        stopTimer()
                    }
                    .font(.title)
                    .foregroundColor(Color(.blue))

                    Text("Random number: \(String(randNum)), interesting: \(String(isNumberInteresting))")
                        .onReceive(timer, perform: { _ in
                            randNum = Int16.random(in: 0..<Int16.max)
                            isNumberInteresting = checkNumber(num: randNum)
                        })
                }
            } else {
                Button("Start") {
                    self.isAnimating = true
                    startTimer()
                }
                .font(.title)
                .foregroundColor(Color(.red))
            }
            if self.isAnimating {
                // Animation
                ZStack {
                    ForEach(0..<10) { waveIteration in
                        Wave(strength: waveStrength, frequency: waveFrequency, phase: phase)
                            .stroke(Color.blue.opacity(Double(waveIteration) / 3), lineWidth: 1.1)
                            .offset(y: CGFloat(waveIteration) * 10)
                    }
                }
                .onReceive(timer) { _ in
                    // withAnimation requires info on how to animate
                    withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
                        self.phase = .pi * 2 // 180 degrees of sine being calculated
                        if isNumberInteresting {
                            waveFrequency = 50.0
                            waveStrength = 50.0
                        } else {
                            waveFrequency = 10.0
                            waveStrength = 10.0
                        }
                    }
                }
                .frame(height: UIScreen.main.bounds.height * 0.8)
            } else {
                // Static line
                ZStack {
                    Line()
                        .stroke(Color.blue)
                }
                .frame(height: UIScreen.main.bounds.height * 0.8)
            }
            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Side notes

In addiction to the problem above any good practice pointers on working with Swift are always welcome.

Upvotes: 2

Views: 1114

Answers (1)

swiftPunk
swiftPunk

Reputation: 1

I made your project works, you can see the changed code // <<: Here!, the issue was there that you did not show the Animation the changed value! you showed just one time! and after that you keep it the same! if you see your code in your question you are repeating self.phase = .pi * 2 it makes no meaning to Animation! I just worked on your ContentView the all project needs refactor work, but that is not the issue here.

struct ContentView: View {
    
    @State private var phase = 0.0
    @State private var waveStrength: Double = 10.0
    @State private var waveFrequency: Double = 10.0
    
    @State var isAnimating: Bool = false
    @State private var randNum: Int16 = 0
    @State private var isNumberInteresting: Bool = false
    
    
    @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    
    @State private var stringOfText: String = String()   // <<: Here!
    
    func stopTimer() {
        
        self.timer.upstream.connect().cancel()
        
        phase = 0.0  // <<: Here!
    }
    
    func startTimer() {
        
        self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(500)) { phase = .pi * 2 }    // <<: Here!
        
    }
    
    
    func checkNumber(num: Int16) -> Bool {
        var isInteresting: Bool = false
        if num % 2 == 0 {
            isInteresting.toggle()
        }
        return isInteresting
    }
    
    
    
    var body: some View {
        
        VStack {
            
            Button(isAnimating ? "Stop" : "Start") {  // <<: Here!
                
                isAnimating.toggle()    // <<: Here!
                
                isAnimating ? startTimer() : stopTimer()   // <<: Here!
                
            }
            .font(.title)
            .foregroundColor(isAnimating ? Color.red : Color.blue)  // <<: Here!
            
            
            ZStack {
                
                if isAnimating {
                    
                    ForEach(0..<10) { waveIteration in
                        Wave(strength: waveStrength, frequency: waveFrequency, phase: phase)
                            .stroke(Color.blue.opacity(Double(waveIteration) / 3), lineWidth: 1.1)
                            .offset(y: CGFloat(waveIteration) * 10)
                    }
                    
                }
                else {
                    
                    Line().stroke(Color.blue)
                    
                }
                
            }
            .frame(height: UIScreen.main.bounds.height * 0.8)
            .overlay(isAnimating ? Text(stringOfText) : nil, alignment: .top)   // <<: Here!
            .onReceive(timer) { _ in

                if isAnimating {  // <<: Here!
                    
                    randNum = Int16.random(in: 0..<Int16.max)        
                    isNumberInteresting = checkNumber(num: randNum)  

                    stringOfText = "Random number: \(String(randNum)), interesting: \(String(isNumberInteresting))" // <<: Here!

                    if isNumberInteresting {
                        waveFrequency = 50.0
                        waveStrength = 50.0
                    } else {
                        waveFrequency = 10.0
                        waveStrength = 10.0
                    }
 
                }
                else {
                    stopTimer() // For safety! Killing Timer in case!        // <<: Here!
                }
  
            }
            .animation(nil, value: stringOfText)  // <<: Here!
            .animation(Animation.linear(duration: 1).repeatForever(autoreverses: false))  // <<: Here!
            .id(isAnimating)  // <<: Here!
            
        }
        
        
    }
}

Upvotes: 2

Related Questions