itsBryan
itsBryan

Reputation: 23

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

Upvotes: 2

Views: 992

Answers (2)

If you really need the ObservedObject or any attribute of it in order to perform any action in case of "OK" action of the alert, you can do a workaround.

struct TimerView: View {
   @ObservedObject var stopwatch = Stopwatch()
   @State var isResetAccepted: 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(isResetAccepted: $isResetAccepted)
        .onChange(of: isResetAccepted) { newValue in
            if newValue {
                isResetAccepetd = false
                stopwatch.reset()               
            }
        }
    }
  }
}

struct CustomAlertView: View {
  
  @Binding var isResetAccepted: Bool
  @State var isAlertPresented: Bool = false
    
    var body: some View {
       Button(action:{
        isAlertPresented.toggle()
       }){
        Text("Toggle Alert")
       }.alert(isPresented: $isAlertPresented){
        Alert(title:Text("Error"),
        message:Text("I  am presented"),
        primaryButton: .destructive(Text("Cancel"), action: {
            self.isResetAccepted = false
            self.isAlertPresented = false                    
        }),
        secondaryButton: .default(Text("OK"), action: {
            self.isResetAccepted = true
            self.isAlertPresented = false
        }))
    }
  }
}

Upvotes: 0

Vaisakh
Vaisakh

Reputation: 2927

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

Upvotes: 4

Related Questions