'DispatchQueue.main.asyncAfter with on/off switch

I whipped up the below struct as a way to alert the user when there's a slow network connection.

When a function is going to make a call to the server, it creates a ResponseTimer. This sets a delayed notification, which only fires if the responseTimer var isOn = true. When my function get's a response back from the server, set responseTimer.isOn = false.

Here's the struct:

struct ResponseTimer {

var isOn: Bool

init() {
    self.isOn = true
    self.setDelayedAlert()
}

func setDelayedAlert() {
    let timer = DispatchTime.now() + 8
    DispatchQueue.main.asyncAfter(deadline: timer) {
        if self.isOn {
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: toastErrorNotificationKey), object: self, userInfo: ["toastErrorCase" : ToastErrorCase.poorConnection])
        }
    }
}

And here's how I'd use it

func getSomethingFromFirebase() {

    var responseTimer = ResponseTimer()

    ref.observeSingleEvent(of: .value, with: { snapshot in
        responseTimer.isOn = false

        //do other stuff
    })
}

Even in cases where the response comes back before the 8 second delay completes, the notification is still fired. What am I doing wrong here??? Is there a better pattern to use for something like this?

Thanks for the help!



Solution 1:[1]

There are a few approaches:

  1. invalidate a Timer in deinit

    Its implementation might look like:

    class ResponseTimer {
        private weak var timer: Timer?
    
        func schedule() {
            timer = Timer.scheduledTimer(withTimeInterval: 8, repeats: false) { _ in // if you reference `self` in this block, make sure to include `[weak self]` capture list, too
                // do something
            }
        }
    
        func invalidate() {
            timer?.invalidate()
        }
    
        // you might want to make sure you `invalidate` this when it’s deallocated just in 
        // case you accidentally had path of execution that failed to call `invalidate`.
    
        deinit {
            invalidate()
        }
    }
    

    And then you can do:

    var responseTimer: ResponseTimer?
    
    func getSomethingFromFirebase() {
        responseTimer = ResponseTimer()
        responseTimer.schedule()
    
        ref.observeSingleEvent(of: .value) { snapshot in
            responseTimer?.invalidate()
    
            //do other stuff
        }
    }
    
  2. Use asyncAfter with DispatchWorkItem, which you can cancel:

    class ResponseTimer {
        private var item: DispatchWorkItem?
    
        func schedule() {
            item = DispatchWorkItem { // [weak self] in // if you reference `self` in this block, uncomment this `[weak self]` capture list, too
                // do something
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: item!)
        }
    
        func invalidate() {
            item?.cancel()
            item = nil
        }
    
        deinit {
            invalidate()
        }
    }
    
  3. Use DispatchTimerSource, which cancels automatically when it falls out of scope:

    struct ResponseTimer {
        private var timer: DispatchSourceTimer?
    
        mutating func schedule() {
            timer = DispatchSource.makeTimerSource(queue: .main)
            timer?.setEventHandler { // [weak self] in // if you reference `self` in the closure, uncomment this
                NotificationCenter.default.post(name: notification, object: nil)
            }
            timer?.schedule(deadline: .now() + 8)
            timer?.activate()
        }
    
        mutating func invalidate() {
            timer = nil
        }
    }
    

In all three patterns, the timer will be canceled when ResponseTimer falls out of scope.

Solution 2:[2]

A better way is to use DispatchSourceTimer which can be cancelled

var timer : DispatchSourceTimer?

func startTimer()
{
    if timer == nil {
        timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
        timer!.schedule(deadline: .now() + .seconds(8))
        timer!.setEventHandler {
            DispatchQueue.main.async {
                NotificationCenter.default.post(name: NSNotification.Name(rawValue: toastErrorNotificationKey), object: self, userInfo: ["toastErrorCase" : ToastErrorCase.poorConnection])
            }
            self.timer = nil
        }
        timer!.resume()
    } 
}

func getSomethingFromFirebase() {

    startTimer()

    ref.observeSingleEvent(of: .value, with: { snapshot in
         self.timer?.cancel()
         self.timer = nil
        //do other stuff
    })
}

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
Solution 2 vadian