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