Cancel a timed event in Swift?
Asked Answered
P

7

81

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?

Photima answered 6/2, 2015 at 6:24 Comment(0)
K
12

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: dispatch_after - GCD in swift?

Karynkaryo answered 6/2, 2015 at 11:39 Comment(6)
Could someone update this to Swift 3.0, please? I've been using this up to 2.3 but after updating to Swift 3.0 the compiler will complain about setting the closure to nil.Straightout
See above but note it's untested and may not be the best way to do it with Swift 3.0. Let me know if it works and I'll update it.Karynkaryo
for me the swift 3 code doesn't work. I have to declare the closures to @escaping ()->VoidOddment
Did you try the Swift 3 version I added last week? I'll edit it to make it clearer which is which.Karynkaryo
Didn't work well for me on Swift 3. I noticed that asyncAfter:execute: doesn't respect deadline sometimes: the clouse is executing later than expected. @DavidLawson's answer worked just fine though.Kazimir
David's answer makes a lot more sense for 3.0. I'd adopted the old 2.0 code for 3.0 but never used or tested it. I'll update my answer to refer to David's.Karynkaryo
D
310

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()
Destiny answered 25/9, 2016 at 7:43 Comment(6)
It seems like a proper way to do that in Swift 3. Should be accepted answer.Kazimir
It depends. It's "nice" and rather clean, but you have to keep track of a variable to cancel it later, possibly an instance variable if you cancel it in some other function later. The NSObject functions handle it for you, so there's no bookkeeping. (Of course, if you're talking about pure Swift, then the NSObject way isn't an option.)Selfimprovement
Note that task.cancel() will cancel all pending DispatchWorkItem events and prevent any FUTURE DispatchWorkItem events from running. If you are trying to create something like a repeating polling timer that turns on and off, you need to create a NEW DispatchWorkItem every time you want to "restart" the timer if you have "cancelled" the task. Essentially, task.cancel() "invalidates" the DispatchWorkItem, it cannot be used again. I suspect internally Apple sets the isCancelled flag for the DispatchWorkItem, and I did not see a way to "clear" that flag for reuse. I think this is a "feature".Weinman
How to keep track of task variable to cancel the task later from some other function ? I guess we need to initialize the task variable globally. but cant figure out how. any leads?Ziguard
@Ziguard you could subclass DispatchWorkItem to override its constructor, so you can set your executable code every time you create a new object. That way, after you call .cancel(), you can just replace your old object with a new one, which will keep on working.Maiocco
This is what I use and I haven't had any problems with it yet. Short, sweet, straight to the point!Nitrobacteria
A
30

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)

Update for SwiftUI

Intialize Timer

private var timer = Timer()

Set up timer

private func setupTimer() {
            // Cancel timer
            if self.timer.isValid {
                self.timer.invalidate()
            }
            // Setup timer
            self.timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false, block: { _ in
                Task {
                    await self.eventTrigger()
                }
            })
        }

Method trigger

private func eventTrigger() async {
       // Do something here
}
Arnold answered 21/3, 2017 at 13:38 Comment(8)
This is my preferred way to handle this, honestly. You don't have to keep a hold on any state to cancel it. NSObject takes care of it for you, so you can cancel and delay a selector all over the place without doing any additional bookkeeping.Selfimprovement
This relies on the ObjC runtime, so not very Swifty at all.Barnaba
Technically, yep, you're right. Fortunately, 99% of Swift dev is for macOS/iOS currently, so it works for now.Selfimprovement
Excellent answer!Nitrobacteria
The delay is not working for me. The func get executed immediately.Cheddite
I was worried about inadvertently cancelling other pending requests on the object, but discovered that there is also a cancelPreviousPerformRequests(withTarget:selector:object:) method. (developer.apple.com/documentation/objectivec/nsobject/…)Thiourea
I used this method to consolidate multiple observer calls. I don't want the observer code to keep firing repeatedly. Using cancelPreviousPerformRequests works really well for me.Troop
It works, with cancelPreviousPerformRequests(withTarget:selector:object:), we could specify which perform request we want to cancel. And remember to use same thread i.e. main thread for it.Tranquilizer
K
12

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: dispatch_after - GCD in swift?

Karynkaryo answered 6/2, 2015 at 11:39 Comment(6)
Could someone update this to Swift 3.0, please? I've been using this up to 2.3 but after updating to Swift 3.0 the compiler will complain about setting the closure to nil.Straightout
See above but note it's untested and may not be the best way to do it with Swift 3.0. Let me know if it works and I'll update it.Karynkaryo
for me the swift 3 code doesn't work. I have to declare the closures to @escaping ()->VoidOddment
Did you try the Swift 3 version I added last week? I'll edit it to make it clearer which is which.Karynkaryo
Didn't work well for me on Swift 3. I noticed that asyncAfter:execute: doesn't respect deadline sometimes: the clouse is executing later than expected. @DavidLawson's answer worked just fine though.Kazimir
David's answer makes a lot more sense for 3.0. I'd adopted the old 2.0 code for 3.0 but never used or tested it. I'll update my answer to refer to David's.Karynkaryo
B
6

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

Bigot answered 8/1, 2019 at 17:5 Comment(0)
P
1

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()
}
Paphian answered 6/2, 2015 at 9:1 Comment(3)
This doesn't actually cancel the timer. It just stops the code in the timer from running.Photima
Isnt that what you wanted? You are able to cancel it with the variable "doIt", so if you don't want the code to be executed, u just set it to false..Paphian
No. The function doSomething still runs no matter what. That's not nearly as useful as the answer I just accepted. Take, for example, the scenario that you started a timer, set the variable to false to 'cancel' it and then started another timer setting the variable back to true. The code would execute when the first timer finished.Photima
B
0

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()
  }
}
Beslobber answered 26/1, 2016 at 7:11 Comment(1)
I just tried using it in the most recent version of Xcode and it seems to be working just fine.Crier
G
0

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.

Gertiegertrud answered 13/5, 2017 at 20:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.