'Alarm Button interacts incorrectly with view - SwiftUI

I'm continuing to develop a timer app for practice purposes and have the basic functions ready so far.

The timer works in such a way that if you click on "Start" the timer simply runs down to 0 and then selects the next player - here you can also the button in the upper middle whether an alarm is played after the timer or not - however, this also stops the timer, although this does not occur in the implementation (see also video below). I hope someone can help me.

For the timer I made a StopWatchManager class:

import Foundation

class StopWatchManager : ObservableObject {
    
    @Published var secondsElapsed : Double = 0.00
    @Published var mode : stopWatchMode = .stopped
    var timer : Timer = Timer()
    
    //Start the timer
    func start() -> Void {
        secondsElapsed = 0.00
        mode = .running
        timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true){ _ in
            self.secondsElapsed += 0.01
        }
    }
    
    //Pause the timer
    func pause() -> Void {
        timer.invalidate()
        mode = .paused
    }
    
    //Stop the timer
    func stop() -> Void {
        timer.invalidate()
        secondsElapsed = 0.00
        self.mode = .stopped
    }
}

enum stopWatchMode{
    case running
    case stopped
    case paused
}

Then I have an overview TimerView which implements some little details and connects the buttons with TimerTextView:

import SwiftUI

struct TimerView: View {
    
    @State private var alarmSoundOn : Bool = true
    @State private var quitGame : Bool = false
    
    //buttons variable for timer
    @State private var startTimer : Bool = false
    @State private var resetTimer : Bool = false
    @State private var pauseTimer : Bool = false
    @State private var quitTimer : Bool = false
    
    var body: some View {
        
        ZStack{
            Color.blue.ignoresSafeArea()
            
            TimerTextView(startTimer: $startTimer, resetTimer: $resetTimer, pauseTimer: $pauseTimer, quitTimer: $quitTimer, playAlarmSound: $alarmSoundOn)
            
            if (!quitGame) {
                VStack{
                    HStack(alignment: .top){
                        
                        //TODO - find bug with Timer
                        Button(action: {
                            alarmSoundOn.toggle()
                        }, label: {
                            if (alarmSoundOn) {
                                Image(systemName: "speaker.circle")
                                    .resizable().frame(width: 30, height: 30)
                            } else {
                                Image(systemName: "speaker.slash.circle")
                                    .resizable().frame(width: 30, height: 30)
                            }
                        }).foregroundColor(.white)
                    }
                    Spacer() 
                }
            }
        }
        
    }
}

And here is my TimerTextView where all the logic with the buttons and the circle happens:

import SwiftUI
import AVFoundation

struct TimerTextView: View {
    
    //timer instance
    @ObservedObject var playTimer : StopWatchManager = StopWatchManager()
    
    //default buttons for the timer
    @Binding var startTimer : Bool
    @Binding var resetTimer : Bool
    @Binding var pauseTimer : Bool
    @Binding var quitTimer : Bool
    
    //variables for circle
    var timerSeconds : Int = 10
    @State private var progress : Double = 1.00
    @State private var endDate : Date? = nil
    
    //seconds of timer as text in the middle of the timer
    @State var textTimerSeconds : Double = 10
    
    //sound for the alarm after the timer exceeded
    @Binding var playAlarmSound : Bool
    @State private var playAlarmAtTheEnd : Bool = true
    let sound : SystemSoundID = 1304
    
    var body: some View {
        
        ZStack{
            if (quitTimer) {
                EmptyView()
            } else {
                
                //Circles---------------------------------------------------
                VStack{
                    VStack{
                        VStack{
                            ZStack{
                                //Circle
                                ZStack{
                                    
                                    Circle()
                                        .stroke(lineWidth: 20)
                                        .foregroundColor(.white.opacity(0.3))
                                    
                                    Circle()
                                        .trim(from: 0.0, to: CGFloat(Double(min(progress, 1.0))))
                                        .stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
                                        .rotationEffect(.degrees(270.0))
                                        .foregroundColor(.white)
                                        .animation(.linear, value: progress)
                                }.padding()
                                
                                VStack{
                                    //Timer in the middle
                                    Text("\(Int(textTimerSeconds.rounded(.up)))")
                                        .font(.system(size: 80))
                                        .bold()
                                        .multilineTextAlignment(.center)
                                        .foregroundColor(.white)
                                }
                                
                            }.padding()
                            //timer responds every milliseconds and calls intern function decrease timer
                                .onReceive(playTimer.$secondsElapsed, perform: { _ in
                                    decreaseTimer()
                                })
                            //pauses the timer
                                .onChange(of: pauseTimer, perform: { change in
                                    if (pauseTimer) {
                                        playTimer.pause()
                                    } else {
                                        endDate = Date(timeIntervalSinceNow: TimeInterval(textTimerSeconds))
                                        playTimer.start()
                                    }
                                })
                            //resets the timer
                                .onChange(of: resetTimer, perform: { change in
                                    if (resetTimer) {
                                        resetTimerToBegin()
                                        resetTimer = false
                                    }
                                })
                            //play alarm sound at the end of the timer
                                .onChange(of: playAlarmSound, perform: { change in
                                    if (playAlarmSound){
                                        playAlarmAtTheEnd = true
                                    } else {
                                        playAlarmAtTheEnd = false
                                    }
                                })
                                .onChange(of: startTimer, perform: { change in                                    startTimerFromBegin()
                                })
                        }
                    }.foregroundColor(Color.black)
                    
                    
                    //----------------- Buttons
                    
                    VStack(spacing: 50){
                        
                        //if isStoped -> show play, reset & quit
                        //if !isStoped -> show pause
                        if(pauseTimer){
                            
                            HStack(alignment: .bottom){
                                
                                Spacer()
                                Spacer()
                                
                                //Play Button
                                Button(action: {
                                    pauseTimer = false
                                }, label: {
                                    VStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 5){
                                        Image(systemName: "play.fill")
                                        Text("Play").font(.callout)
                                    }
                                })
                                    .font(.title2)
                                    .frame(width: 60, height: 60, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                                    .padding(3.0)
                                    .buttonStyle(BorderlessButtonStyle())
                                
                                Spacer()
                                
                                //Reset Button
                                Button(action: {
                                    resetTimer = true
                                }, label: {
                                    VStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 5){
                                        Image(systemName: "gobackward")
                                        Text("Reset").font(.callout)
                                    }
                                })
                                    .font(.title2)
                                    .frame(width: 60, height: 60, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                                    .padding(3.0)
                                    .buttonStyle(BorderlessButtonStyle())
                                
                                Spacer()
                                
                                //Quit Button
                                Button(action: {
                                    quitTimer = true
                                }, label: {
                                    VStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 5){
                                        Image(systemName: "flag.fill")
                                        Text("Exit").font(.callout)
                                    }
                                })
                                    .font(.title2)
                                    .frame(width: 60, height: 60, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                                    .padding(3.0)
                                    .buttonStyle(BorderlessButtonStyle())
                                
                                Spacer()
                                Spacer()
                            }
                        } else if (startTimer) {
                            
                            //Pause Button
                            Button(action: {
                                pauseTimer = true
                            }, label: {
                                VStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 5){
                                    Image(systemName: "pause.fill")
                                    Text("Pause").font(.callout)
                                }
                            })
                                .font(.title2)
                                .frame(width: 60, height: 60, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                                .padding(3.0)
                                .buttonStyle(BorderlessButtonStyle())
                            
                        } else {
                            
                            //Play Button
                            Button(action: {
                                pauseTimer = false
                                startTimer = true
                            }, label: {
                                VStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 5){
                                    Image(systemName: "play.fill")
                                    Text("Start").font(.callout)
                                }
                            })
                                .font(.title2)
                                .frame(width: 60, height: 60, alignment: .center)
                                .padding(3.0)
                                .buttonStyle(BorderlessButtonStyle())
                        }
                    }.foregroundColor(.white)
                }
            }
        }
    }
    
    //FUNCTIONS --------------------------------
    
    private func startTimerFromBegin() -> Void{
        endDate = Date(timeIntervalSinceNow: TimeInterval(timerSeconds))
        playTimer.start()
    }
    
    private func resetTimerToBegin() -> Void {
        endDate = Date(timeIntervalSinceNow: TimeInterval(timerSeconds))
        progress = 1.0
        textTimerSeconds = Double(timerSeconds)
    }
    
    private func decreaseTimer() -> Void{
        guard let endDate = endDate else { print("decreaseTimer() was returned"); return }
        progress = max(0, endDate.timeIntervalSinceNow / TimeInterval(timerSeconds))
        textTimerSeconds -= 0.01
        if endDate.timeIntervalSinceNow <= 0 {
            if (playAlarmAtTheEnd) {
                AudioServicesPlayAlertSound(sound)
            }
        }
    }
}

enter image description here



Solution 1:[1]

You are instantiating your playTimer in the subview ... That will reset it when the view is redrawn. Instead you should do this in the parent view and pass it down.

Also you should use @StateObject to instantiate.

struct TimerView: View {
    
    //timer instance HERE
    @StateObject var playTimer : StopWatchManager = StopWatchManager()
    ...

pass down to subview

        // pass timer here
        TimerTextView(playTimer: playTimer, startTimer: $startTimer, resetTimer: $resetTimer, pauseTimer: $pauseTimer, quitTimer: $quitTimer, playAlarmSound: $alarmSoundOn)

Subview:

struct TimerTextView: View {
    
    //passed timer
    @ObservedObject var playTimer : StopWatchManager
    ...

One more thing: Firing the timer every 0.01 seconds is too much. You should go to 0.1

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ChrisR