警报按钮与视图交互不正确 - SwiftUI

Alarm Button interacts incorrectly with view - SwiftUI

我正在继续开发用于练习目的的计时器应用程序,目前基本功能已准备就绪。

计时器的工作方式是,如果您点击“开始”,计时器就会简单地减到 0,然后选择下一位玩家 - 在这里您还可以选择中间上方的按钮是否在之后播放警报计时器与否 - 然而,这也会停止计时器,尽管这不会发生在实现中(另请参见下面的视频)。我希望有人能帮助我。

我做了一个定时器 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
}

然后我有一个概述 TimerView,它实现了一些小细节并将按钮与 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() 
                }
            }
        }
        
    }
}

这是我的 TimerTextView 按钮和圆圈的所有逻辑发生的地方:

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)
            }
        }
    }
}

您正在子视图中实例化您的 playTimer ...这将在重绘视图时重置它。相反,您应该在父视图中执行此操作并将其传递下去。

另外你应该使用@StateObject来实例化。

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

向下传递到子视图

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

子视图:

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

还有一件事:每 0.01 秒触发一次计时器太多了。你应该去 0.1