Semi related question: SwiftUI ActionSheet does not dismiss when timer is running
I am currently experiencing an issue with alerts in a project that I am working on. Presented alerts will not dismiss when there is a timer running in the background. Most of the time it requires several clicks of the dismissal button to disappear. I have recreated this issue with as little overhead as possible in a sample project.
My primary project has this issue when trying to display an alert on a different view but I could not reproduce that issue in the sample project. The issue can be reliably replicated by toggling the alert on the same view that the timer is running. I have also tested by removing the binding from the text field to stop the text field view from updating. The alert still fails to dismiss on the first click. I am unsure if there is a way to work around this and am looking for any advice possible.
Xcode 13.0/iOS 15.0 and occurs in iOS 14.0 also
Timerview.swift
struct TimerView: View {
@ObservedObject var stopwatch = Stopwatch()
@State var isAlertPresented:Bool = false
var body: some View {
VStack{
Text(String(format: "%.1f", stopwatch.secondsElapsed))
.font(.system(size: 70.0))
.minimumScaleFactor(0.1)
.lineLimit(1)
Button(action:{
stopwatch.actionStartStop()
}){
Text("Toggle Timer")
}
Button(action:{
isAlertPresented.toggle()
}){
Text("Toggle Alert")
}
}
.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),message:Text("I am presented"))
}
}
}
Stopwatch.swift
class Stopwatch: ObservableObject{
@Published var secondsElapsed: TimeInterval = 0.0
@Published var mode: stopWatchMode = .stopped
func actionStartStop(){
if mode == .stopped{
start()
}else{
stop()
}
}
var timer = Timer()
func start() {
secondsElapsed = 0.0
mode = .running
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.secondsElapsed = 0.1
}
}
func stop() {
timer.invalidate()
mode = .stopped
}
enum stopWatchMode {
case running
case stopped
}
}
Edit: Moving the button to a custom view solves the initial problem but is there a solution for when the button needs to interact with the Observable object?
Button(action:{
do{
try stopwatch.actionDoThis()
}catch{
isAlertPresented = true
}
}){
Text("Toggle Alert")
}.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),message:Text("I am presented"))
CodePudding user response:
Every time timer runs UI will recreate, since "secondsElapsed" is an observable object. SwiftUI will automatically monitor for changes in "secondsElapsed", and re-invoke the body property of your view. In order to avoid this we need to separate the button and Alert to another view like below.
struct TimerView: View {
@ObservedObject var stopwatch = Stopwatch()
@State var isAlertPresented:Bool = false
var body: some View {
VStack{
Text(String(format: "%.1f", stopwatch.secondsElapsed))
.font(.system(size: 70.0))
.minimumScaleFactor(0.1)
.lineLimit(1)
Button(action:{
stopwatch.actionStartStop()
}){
Text("Toggle Timer")
}
CustomAlertView(isAlertPresented: $isAlertPresented)
}
}
}
struct CustomAlertView: View {
@Binding var isAlertPresented: Bool
var body: some View {
Button(action:{
isAlertPresented.toggle()
}){
Text("Toggle Alert")
}.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),message:Text("I am presented"))
}
}
}