我正在繼續開發一個用于練習目的的計時器應用程式,并且到目前為止已經準備好基本功能。
計時器的作業方式是,如果您單擊“開始”,計時器會簡單地運行到 0,然后選擇下一個玩家 - 在這里,您還可以在中間上方的按鈕,無論是否在計時器之后播放警報 -但是,這也會停止計時器,盡管這在實作中不會發生(另請參見下面的視頻)。我希望有一個人可以幫助我。
對于計時器我做了一個StopWatchManager類:
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)
}
}
}
}

uj5u.com熱心網友回復:
您正在子視圖中實體化您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
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/441307.html
