Search code examples
iosswiftswiftuialert

SwiftUI Alert does not dismiss when timer is running


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

Solution

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