警报按钮与视图交互不正确 - 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
我正在继续开发用于练习目的的计时器应用程序,目前基本功能已准备就绪。
计时器的工作方式是,如果您点击“开始”,计时器就会简单地减到 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