Cancel a timed event in Swift?

SwiftEvents

Swift Problem Overview


I want to run a block of code in 10 seconds from an event, but I want to be able to cancel it so that if something happens before those 10 seconds, the code won't run after 10 seconds have gone by.

I've been using this, but it's not cancellable:

static func delay(delay:Double, closure:()->()) {
  dispatch_after(
    dispatch_time(
      DISPATCH_TIME_NOW,
      Int64(delay * Double(NSEC_PER_SEC))
    ),
    dispatch_get_main_queue(), closure
  )
}

How can I accomplish this?

Swift Solutions


Solution 1 - Swift

Swift 3 has DispatchWorkItem:

let task = DispatchWorkItem { print("do something") }

// execute task in 2 seconds
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2, execute: task)

// optional: cancel task
task.cancel()

Solution 2 - Swift

Update for Swift 3.0

Set Perform Selector

perform(#selector(foo), with: nil, afterDelay: 2)

foo method will call after 2 seconds

 func foo()
 {
         //do something
 }

To cancel pending method call

 NSObject.cancelPreviousPerformRequests(withTarget: self)

Solution 3 - Swift

Try this (Swift 2.x, see David's answer below for Swift 3):

typealias dispatch_cancelable_closure = (cancel : Bool) -> ()

func delay(time:NSTimeInterval, closure:()->()) ->  dispatch_cancelable_closure? {

    func dispatch_later(clsr:()->()) {
        dispatch_after(
            dispatch_time(
                DISPATCH_TIME_NOW,
                Int64(time * Double(NSEC_PER_SEC))
            ),
            dispatch_get_main_queue(), clsr)
    }

    var closure:dispatch_block_t? = closure
    var cancelableClosure:dispatch_cancelable_closure?

    let delayedClosure:dispatch_cancelable_closure = { cancel in
        if let clsr = closure {
            if (cancel == false) {
                dispatch_async(dispatch_get_main_queue(), clsr);
            }
        }
        closure = nil
        cancelableClosure = nil
    }

    cancelableClosure = delayedClosure

    dispatch_later {
        if let delayedClosure = cancelableClosure {
            delayedClosure(cancel: false)
        }
    }

    return cancelableClosure;
}

func cancel_delay(closure:dispatch_cancelable_closure?) {
    if closure != nil {
        closure!(cancel: true)
    }
}

// usage
let retVal = delay(2.0) {
    println("Later")
}
delay(1.0) {
    cancel_delay(retVal)
}

From Waam's comment here: https://stackoverflow.com/questions/24034544/dispatch-after-gcd-in-swift/24318861#24318861

Solution 4 - Swift

You need to do this:

class WorkItem {

private var pendingRequestWorkItem: DispatchWorkItem?

func perform(after: TimeInterval, _ block: @escaping VoidBlock) {
    // Cancel the current pending item
    pendingRequestWorkItem?.cancel()
    
    // Wrap the request in a work item
    let requestWorkItem = DispatchWorkItem(block: block)
    
    pendingRequestWorkItem = requestWorkItem

    DispatchQueue.main.asyncAfter(deadline: .now() + after, execute: 
    requestWorkItem)
}
}

// Use

lazy var workItem = WorkItem()

private func onMapIdle() {

    workItem.perform(after: 1.0) {

       self.handlePOIListingSearch()
    }
}

References

Link swiftbysundell

Link git

Solution 5 - Swift

This should work:

var doIt = true
var timer = NSTimer.scheduledTimerWithTimeInterval(10, target: self, selector: Selector("doSomething"), userInfo: nil, repeats: false)

//you have now 10 seconds to change the doIt variable to false, to not run THE CODE

func doSomething()
{
    if(doIt)
    {
        //THE CODE
    }
    timer.invalidate()
}

Solution 6 - Swift

I use @sas 's method in some projects, somehow this doesn't work anymore, maybe something changed after Swift 2.1.1. value copy instead of pointer?

the easiest work around method for me is:

var canceled = false
delay(0.25) {
  if !canceled {
    doSomething()
  }
}

Solution 7 - Swift

For some reason, NSObject.cancelPreviousPerformRequests(withTarget: self) was not working for me. A work around I thought of was coming up with the max amount of loops I'd allow and then using that Int to control if the function even got called.

I then am able to set the currentLoop value from anywhere else in my code and it stops the loop.

//loopMax = 200
var currentLoop = 0
        
func loop() {      
  if currentLoop == 200 {      
     //do nothing.
  } else {
     //perform loop.
    
     //keep track of current loop count.
     self.currentLoop = self.currentLoop + 1
        
     let deadline = DispatchTime.now() + .seconds(1)
     DispatchQueue.main.asyncAfter(deadline: deadline) {
        
     //enter custom loop parameters
     print("i looped")
        
     self.loop()
   }
        
}

and then elsewhere in your code you can then

func stopLooping() {
    
    currentLoop = 199
    //setting it to 199 allows for one last loop to happen. You can adjust based on the amount of loops you want to be able to do before it just stops. For instance you can set currentLoop to 195 and then implement a fade animation while loop is still happening a bit.
}

It's really quite dynamic actually. For instance you can see if currentLoop == 123456789, and it will run infinitely (pretty much) until you set it to that value somewhere else in your code. Or you can set it to a String() or Bool() even, if your needs are not time based like mine were.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMax HudsonView Question on Stackoverflow
Solution 1 - SwiftDavid LawsonView Answer on Stackoverflow
Solution 2 - SwiftSaumil ShahView Answer on Stackoverflow
Solution 3 - SwiftsasView Answer on Stackoverflow
Solution 4 - SwiftjmarkstarView Answer on Stackoverflow
Solution 5 - SwiftDejan SkledarView Answer on Stackoverflow
Solution 6 - SwiftrexshengView Answer on Stackoverflow
Solution 7 - SwiftFelecia GenetView Answer on Stackoverflow