In Swift 3 I have a loop which can be canceled by user pressing a button. Within the loop some checks are made. After the check, the task can sleep for a minute. But when calling the task with
let delayQueue = DispatchQueue(label: "com.myApp.queue3", qos: .utility)
let additionalTime: DispatchTimeInterval = .seconds(3)
repeat {
delayQueue.asyncAfter(deadline: .now() + additionalTime) { self.update() }
} while !self.stop
the loop itself needs to run all the time waiting for the user
"stop", indicates, that user clicked on stop button.
Is that waste of CPU power? How could I avoid this loop to be done all the time?
You should use Timer instead.
var timer: Timer?
let timeInterval: TimeInterval = 3
func didPressCancelButton() {
timer?.invalidate()
}
func beginUpdates() {
timer = Timer.scheduledTimer(
timeInterval: timeInterval,
target: self,
selector: #selector(self.update),
userInfo: nil,
repeats: true
);
}
func update() {
print("Updated")
}
Instead of delaying execution in thread with an outer loop you can put your loop in thread instead and make it to sleep.
import Foundation
class YourUpdatingClass {
private let updateQueue: OperationQueue
init() {
updateQueue = OperationQueue()
updateQueue.name = "com.myApp.queue3"
updateQueue.qualityOfService = .utility
}
private var updateOperation: BlockOperation?
#IBAction func startUpdating() {
guard updateOperation == nil else {
// In case if updating already started
return
}
updateOperation = BlockOperation { [weak self] in
while true {
Thread.sleep(forTimeInterval: 3)
self?.update()
}
}
updateQueue.addOperation(updateOperation!) // we just created updateOperation, so we can use `!`, but use it with caution
}
#IBAction func stopUpdating() {
updateOperation?.cancel()
updateOperation = nil
}
private func update() {
print("update") // Whatever your update does
}
}
You updating is contained in eternal while loop which takes a nap every 3 seconds.
Stopping is managed by cancelling operation, instead of checking some var in the loop.
Related
I got a function such as scrollViewDidScroll that can trigger many times. And I need to call function loadMoreDataFromRemoteServerIfNeed only single time. How could I do this more elegantly without using any "flag" variables. Maybe I should use DispathGroup|DispatchWorkItem?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
loadMoreDataFromRemoteServerIfNeed()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
DispatchQueue.global(qos: .background).async {
sleep(2)
DispatchQueue.main.async {
// <Insert New Data>
}
}
}
The thing that you are trying to describe — "Do this, but only if you are not told to do it again any time in the next 2 seconds" — has a name. It's called debouncing. This is a well-solved problem in iOS programming, so now that you know its name, you can do a search and find some of the solutions.
While I'm here telling you about this, here's a solution you might not know about. Debouncing is now built in to iOS functionality! Starting in iOS 13, it's part of the Combine framework. I'm now using Combine all over the place: instead of notifications, instead of GCD, instead of Timer objects, etc. It's great!
Here's a Combine-based solution to this type of problem. Instead of a scroll view, suppose we have a button hooked up to an action handler, and we don't want the action handler to do its task unless 2 seconds has elapsed since the last time the user tapped the button:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
#IBAction func doButton(_ sender: Any) {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.doSomething() }
}
self.pipelineStart.send()
}
func doSomething() {
print("I did it!")
}
I'm sure you can readily see how to adapt that to your own use case:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.loadMoreDataFromRemoteServerIfNeed()
}
self.pipelineStart.send()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
// <Insert New Data>
}
You can create a flag from DispatchWorkItem to observe loading state e.g.:
var item: DispatchWorkItem?
func loadMoreDataFromRemoteServerIfNeed() {
assert(Thread.isMainThread)
guard item == nil else { return }
item = DispatchWorkItem {
print("loading items")
Thread.sleep(forTimeInterval: 2)
DispatchQueue.main.async {
item = nil
print("insert items")
}
}
DispatchQueue.global().async(execute: item!)
}
NOTE: to synchronise item var you must change its value on the same thread for instance the main thread.
Yes, you could use DispatchWorkItem, keep a reference to the old one, and cancel prior one if necessary. If you were going to do that, I might consider Operation, too, as that handles cancelation even more gracefully and has other advantages.
But that having been said, given that the work that you are dispatching is immediately sleeping for two seconds, this might suggest a completely different pattern, namely a Timer. You can schedule your timer, invalidating previously scheduled timers, if any:
weak var timer: Timer?
func loadMoreDataFromRemoteServerIfNeed() {
// cancel old timer if any
timer?.invalidate()
// schedule what you want to do in 2 seconds
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
// <Insert New Data>
}
}
FWIW, if you ever find yourself sleeping, you should general consider either timers or asyncAfter. This avoids tying up the global queue’s worker thread. Sleeping is an inefficient pattern.
In this case, keeping a weak reference to the prior timer (if any) is probably the best pattern.
I'm trying to create a delay inside a while loop. I'm fairly new to this and it's currently just not working. It never fires even once with the dispatch delay, but if I remove the delay it fires repeatedly.
Basically what I'm doing is checking if the velocity of nodes in a SKScene is still moving, if they're still moving, don't end the game. But once they've slowed down, end the game.
func RemainingNodeCheck (complete:() -> Void) {
CountVelocites()
if (IdleVelocity.max()!.isLess(than: 1.0)) {
complete()
} else {
print("Velocity too high, begin wait...")
while !(IdleVelocity.max()?.isLess(than: 1.0))! {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) {
print("Second passed")
self.CountVelocites()
}
if (IdleVelocity.max()!.isLess(than: 1.0)) {
break
}
}
print("Velocity Calmed down")
complete()
}
}
I believe this might be something to do with threads? Or it's actually just telling the delay to begin waiting for one second so many times that it never gets to call?
UPDATE: I would use a timer, but the RemaingNodeCheck is being called from another part and it's waiting for RemainingNodeCheck to send back complete()
You never want to "wait". But you can set up a repeating timer that checks for some condition, and if so, calls the complete closure (invalidating the timer, if you want). E.g.
class ViewController: UIViewController {
var idleVelocity: ...
weak var timer: Timer?
deinit {
timer?.invalidate()
}
func startCheckingVelocity(complete: #escaping () -> Void) {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self, let maxVelocity = self.idleVelocity.max() else { return }
if maxVelocity < 1 {
timer.invalidate()
complete()
return
}
print("velocity too high...")
}
}
}
I can't figure out how to make dispatch timer work repeatedly in Swift 3.0. My code:
let queue = DispatchQueue(label: "com.firm.app.timer",
attributes: DispatchQueue.Attributes.concurrent)
let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: UInt(0)),
queue: queue)
timer.scheduleRepeating(deadline: DispatchTime.now(),
interval: .seconds(5),
leeway: .seconds(1)
)
timer.setEventHandler(handler: {
//a bunch of code here
})
timer.resume()
Timer just fires one time and doesn't repeat itself like it should be. How can I fix this?
Make sure the timer doesn't fall out of scope. Unlike Timer (where the RunLoop on which you schedule it keeps the strong reference until the Timer is invalidated), you need to maintain your own strong reference to your GCD timers, e.g.:
private var timer: DispatchSourceTimer?
private func startTimer() {
let queue = DispatchQueue(label: "com.firm.app.timer", attributes: .concurrent)
timer = DispatchSource.makeTimerSource(queue: queue)
timer?.setEventHandler { [weak self] in // `[weak self]` only needed if you reference `self` in this closure and you want to prevent strong reference cycle
print(Date())
}
timer?.schedule(deadline: .now(), repeating: .seconds(5), leeway: .milliseconds(100))
timer?.resume()
}
private func stopTimer() {
timer = nil
}
In my application the user can press a button. That in turn leads to a function call which is showed below:
In ViewController.Swift
#IBAction func pickMeUpButton(sender: AnyObject) {
sendPushNotificationController().sendPushNotification("sendRequest",userInfo: defaults.stringForKey("x73")!, userInf23: defaults.stringForKey("x23")! )
locationController.getLocationForShortTime() // --> here i want the timer to finish the 5 seconds before proceeding
activityIndicator.center = self.view.center
activityIndicator.startAnimating()
self.view.addSubview(activityIndicator)
//activityIndicator.stopAnimating()
}
And this is the class function where the call is being made to
In getUserLocation.swift
func initManager(){
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
}
func getLocationForShortTime(){
initManager()
timer = NSTimer.scheduledTimerWithTimeInterval(5, target: self, selector: "stopGettingLocation", userInfo: nil, repeats: false)
}
func stopGettingLocation(){
locationManager.stopUpdatingLocation()
}
So this will make the application get the users location for 5 seconds and then the timer will stop the updates. What i want to do is when the five seconds has elapsed and the location update stops THEN i would like the calling function to proceed to next line.
I though of some solutions using boolean, but it is not a nice solution. Im thinking there might be a better way to do this?
Others have told you what to do, but not why.
You need to adjust your thinking.
With an event-driven device like an iPhone/iPad, you can't stop processing on the main thread for 5 seconds. The UI would lock up, and after a couple of seconds the system would kill your app as being hung.
Instead, what you do is to invoke a block of code (a closure) after a delay.
You could rewrite your function like this:
#IBAction func pickMeUpButton(sender: AnyObject)
{
sendPushNotificationController().sendPushNotification("sendRequest",
userInfo: defaults.stringForKey("x73")!,
userInf23: defaults.stringForKey("x23")! )
initManager()
//Start the activity indicator during the delay
activityIndicator.center = self.view.center
self.view.addSubview(activityIndicator)
activityIndicator.startAnimating()
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, Int64(5.0 * Double(NSEC_PER_SEC))),
dispatch_get_main_queue())
{
//The code in the braces gets run after the delay value
locationManager.stopUpdatingLocation()
activityIndicator.stopAnimating()
}
//dispatch_after returns immediately, so code here will run before
//the delay period passes.
}
That button action code will:
Call initManager to start the location manager running.
Immediately create an activity indicator, add it to the view controller's content view, and start it spinning.
Then, the call to dispatch_after will wait for 5 seconds before running the code in the braces, which will stop the location manger and stop the activity indicator.
For delaying a function-call you can use dispatch_after. It's syntax is a little bit ugly so you can also use this delay function:
/// delays the execution of the passed function
func delay(delay: Double, closure: ()->()) {
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))),
dispatch_get_main_queue(),
closure)
}
// calling directly (locationManager gets captured; which is in this case negligible to consider)
delay(5.0, closure: locationManager.stopUpdatingLocation)
// or indirect through your function
delay(5.0, closure: stopGettingLocation)
Pass the closure to getLocationForShortTime. The one that should be run once the thing is finished. I can't really test the code, but it's probably something like:
class Handler { // <- This is the wrapper class for completion closures
private let run: Void -> Void
init(_ run: Void -> Void) { self.run = run }
}
lazy var locationManager: CLLocationManager! = self.lazyLocationManager()
func lazyLocationManager() -> CLLocationManager {
let _locationManager = CLLocationManager()
_locationManager.delegate = self
_locationManager.desiredAccuracy = kCLLocationAccuracyBest
_locationManager.requestAlwaysAuthorization()
return _locationManager
}
func getLocationQuick(onComplete: Void -> Void) { // <- This takes the closure
locationManager.startUpdatingLocation()
let timer = NSTimer.scheduledTimerWithTimeInterval(
5,
target: self,
selector: "gotLocationQuick:",
userInfo: Handler(onComplete), // <- Here you wrap the completion closure
repeats: false // and pass it to the timer
)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)
}
func gotLocationQuick(timer: NSTimer) {
locationManager.stopUpdatingLocation()
let completionHandler = timer.userInfo as! Handler
completionHandler.run()
}
#IBAction func pickMeUpButton(sender: AnyObject) {
sendPushNotificationController().sendPushNotification(
"sendRequest",
userInfo: defaults.stringForKey("x73")!,
userInf23: defaults.stringForKey("x23")!
)
activityIndicator.center = self.view.center
self.view.addSubview(activityIndicator)
activityIndicator.startAnimating()
locationController.getLocationQuick() { // <- You post the request for
activityIndicator.stopAnimating() // geolocation and indicate the code
} // to be run once it's finished
}
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 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()
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)
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?
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
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()
}
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()
}
}
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.