How to suspend a work item on the main queue - swift

I want to know if it is possible to suspend and then resume a work item on the main queue whilst maintaining the '.asyncAfter' time. If not, is there a workaround to achieve this?
At a certain point, I queue up the following DispatchWorkItem:
dispatchWorkItem = DispatchWorkItem(qos: .userInteractive, block: {
self.view.backgroundColor = UIColor.workoutBackgroundColor
self.runTimer()
self.timerButton.animateableTrackLayer.removeAnimation(forKey: "strokeEndAnimation")
self.isRestState = false
})
I queue this up using:
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: self.dispatchWorkItem))
(delayTime being a parameter to the function)
Now, the problem I am running into is how can I suspend this work item if the user performs a 'pause' action in my app.
I have tried using the DispatchQueue.main.suspend() method but the work item continues to execute after the specified delay time. From what I have read, this method should suspend the queue and this queued work item since it is not being executed. (Please correct me if I am wrong there!)
What I need to achieve is the work item is 'paused' until the user performs the 'resume' action in the app which will resume the work item from where the delay time left off.
This works on background queues that I have created when I do not need to make UI updates; however, on the main queue is appears to falter.
One workaround I have considered is when the user performs the pause action, storing the time left until the work item was going to be executed and re-adding the work item to the queue with that time on the resume action. This seems like a poor quality approach and I feel there is a more appropriate method to this.
On that, is it possible to create a background queue that on execution, executes a work item on the main queue?
Thanks in advance!

On that, is it possible to create a background queue that on execution, executes a work item on the main queue?
You are suggesting something like this:
var q = DispatchQueue(label: "myqueue")
func configAndStart(seconds:TimeInterval, handler:#escaping ()->Void) {
self.q.asyncAfter(deadline: .now() + seconds, execute: {
DispatchQueue.main.async(execute: handler())
})
}
func pause() {
self.q.suspend()
}
func resume() {
self.q.resume()
}
But my actual tests seem to show that that won't work as you desire; the countdown doesn't resume from where it was suspended.
One workaround I have considered is when the user performs the pause action, storing the time left until the work item was going to be executed and re-adding the work item to the queue with that time on the resume action. This seems like a poor quality approach and I feel there is a more appropriate method to this.
It isn't poor quality. There is no built-in mechanism for pausing a dispatch timer countdown, or for introspecting the timer, so if you want to do the whole thing on the main queue your only recourse is just what you said: maintain your own timer and the necessary state variables. Here is a rather silly mockup I hobbled together:
class PausableTimer {
var t : DispatchSourceTimer!
var d : Date!
var orig : TimeInterval = 0
var diff : TimeInterval = 0
var f : (()->Void)!
func configAndStart(seconds:TimeInterval, handler:#escaping ()->Void) {
orig = seconds
f = handler
t = DispatchSource.makeTimerSource()
t.schedule(deadline: DispatchTime.now()+orig, repeating: .never)
t.setEventHandler(handler: f)
d = Date()
t.resume()
}
func pause() {
t.cancel()
diff = Date().timeIntervalSince(d)
}
func resume() {
orig = orig-diff
t = DispatchSource.makeTimerSource()
t.schedule(deadline: DispatchTime.now()+orig, repeating: .never)
t.setEventHandler(handler: f)
t.resume()
}
}
That worked in my crude testing, and seems to be interruptible (pausable) as desired, but don't quote me; I didn't spend much time on it. The details are left as an exercise for the reader!

Related

Game Center Turn Timeout for Multiplayer GAmes

I have created a turn based multiplayer board game using Swift and Game Center that works pretty well. One of the last items I would like to add is a way to keep a player from abandoning a game near the end if they know they are going to lose. It seems like the turnTimeout portion of the endTurn function is built in especially for this purpose, but I cannot get it to work. My endTurn function is below:
func endTurn(_ model: GameModel, completion: #escaping CompletionBlock) {
guard let match = currentMatch else {
completion(GameCenterHelperError.matchNotFound)
return
}
do {
let currenParticipantIndex: Int = (match.participants.firstIndex(of: match.currentParticipant!))!
let nextPerson = [match.participants[(currenParticipantIndex+1) % match.participants.count]]
print("end turn, next participant \(String(describing: nextPerson[0].player?.alias))")
match.endTurn(
withNextParticipants: nextPerson,
turnTimeout: 15,
match: try JSONEncoder().encode(model),
completionHandler: completion
)
} catch {completion(error)}
}
This function takes into account the advice from Anton in the comment of the answer to this question:
Trying to set a time limit on my Game Center game
to update the array of nextParticipant players so that the end of the array is never reached. I've also tried to account for this in my testing by having both player 1 and player 2 delay the end of their turn to see if it would fire (The game is a 2 player game only)
This should also answer this question:
Game Center turn timeouts
The documentation says:
timeoutDate: The time at which the player must act before forfeiting a turn. Your game decides what happens when a turn is forfeited. For some games, a forfeited turn might end the match. For other games, you might choose a reasonable set of default actions for the player, or simply do nothing.
https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/GameKit_Guide/ImplementingaTurn-BasedMatch/ImplementingaTurn-BasedMatch.html
Unfortunately I am unable to get the turnTimeout function to fire at all. I have done a fair amount of research and I found no definitive answer to what function is actually called when it fires (i.e. the player takes longer than the allotted time to take their turn). I would expect that the same function is called for a timeout as a regular call of the endTurn function and the below player listener is called:
func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool) {
if let vc = currentMatchmakerVC {
currentMatchmakerVC = nil
vc.dismiss(animated: true)
}
print("received turn event")
if !didBecomeActive {
print("\n\n\n player listener did become active")
print("match did change")
NotificationCenter.default.post(name: .matchDidChange, object: match)
} else if didBecomeActive {
print("present game")
NotificationCenter.default.post(name: .presentGame, object: match)
}
}
I am able to get the player listener (received turn event) to fire when endTurn is specifically called from the game, but I do not see anything that is called when the turnTimeout event triggers. If it was the player listener I would see the print statements in the console as well as the notification on the next player's device.
The GKTurnTimeoutDefault is 604,800 and is a Time Interval which I did some research on and arrived at the conclusion that it is in seconds, which is 7 days. I changed it to 0.00001, 15, 2000 and a few values in between but I wasn't able to get it to fire.
I also found the below, but the first has no answer and the second only says the turn timeouts probably warrants its own full answer:
Game Center Turnbased Game turn timeout
How to detect when Game Center turn based match has ended in iOS9?
I am thinking that my mistake is probably that I am unable to find the function that is called when the turn timeout fires, although I might be mistaken on the Time Interval values that I'm putting in there as well.
Thank you for taking the time to review my question :)

iMac freezes after while loop

I'm creating a game in xcode. The winner will have a negative score or 0. Everything went well but now I want those negative points to be added to his/hers opponent. I used this code:
while (activePlayer.score < 0) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.activePlayer.score += 1
self.activePlayer.scoreLabel.text = String (self.activePlayer.score)
self.notActivePlayer.score += 1
self.notActivePlayer.scoreLabel.text = String (self.notActivePlayer.score)
}
}
When I now run the Simulator it freezes when it comes to this part. The worst thing is my whole iMac freezes. It's becoming so extremely slow I have to wait like 10 minutes to close the simulator and getting some speed back.
My simple conclusion is this code is wrong. But why?
I want to player to see the score change that's why the label text will be updated after every point added to the score.
Your while loop is a very bad idea there. What do you expect it to do? On first iteration you schedule an asnyc task, then the iteration is complete and the next task is scheduled, etc.
You will have a couple of thousand async task scheduled in the first split second the loop is running.
If you want to animate the change you should do it by scheduling the next task after the first finished. The following is a general way of doing that, I have not run it in Playground:
func schedule() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.activePlayer.score += 1
self.activePlayer.scoreLabel.text = String (self.activePlayer.score)
self.notActivePlayer.score += 1
self.notActivePlayer.scoreLabel.text = String (self.notActivePlayer.score)
if (activePlayer.score < 0) {
self.schedule()
}
}
}
As Whirlwind correctly pointed out using dispatch in sprite-kit makes you break out of the present game-loop. You can do the same thing via SKActions and repeatedly create new actions after one completed.

Timer not firing every second on WatchKit

This timer isn't firing every second, when I check the log and UI it seems to be firing every 3-4 seconds.
func startTimer() {
print("start timer")
timer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(timerDidFire),
userInfo: nil,
repeats: true)
}
func timerDidFire(timer: Timer) {
print("timer")
updateLabels()
}
Is this just something that is going to happen on the Watch due to lack of capabilities, or is there something wrong in my code?
Here is the log if needed:
0.0396000146865845
3.99404102563858
7.97501903772354
11.9065310359001
EDIT:
And for clarification, what I'm updating every second is the workout timer, so it needs to be updated every second that ticks by.
If your app is busy doing something else, which blocks or delays the run loop from checking that the fire time has repeatedly passed, the timer will only fire once during that period:
A repeating timer always schedules itself based on the scheduled firing time, as opposed to the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.
As an aside, it may be more efficient to update your UI based on a response to a change (e.g., observation), or reaction to an event (e.g., completion handler).
This avoids creating busy work for the app when it's driven to check yet doesn't actually have a UI update to perform, if nothing has changed during the timer interval.
It also prevents multiple changes within the fire interval from being ignored, since a timer-driven pattern would only be displaying the last change in the UI.
Consider using a WKInterfaceTimer label in place of the label that you are using to show the timing:
A WKInterfaceTimer object is a special type of label that displays a
countdown or count-up timer. Use a timer object to configure the
amount of time and the appearance of the timer text. When you start
the timer, WatchKit updates the displayed text automatically on the
user’s Apple Watch without further interactions from your extension.
Apple Docs.
WatchOS will then take responsibility for keeping this up-to-date. The OS handles the label for you, but you have to keep track of the elapsed time: you just need to set an NSDate to do that (see example below).
Sample Code.
In your WKInterfaceController subclass:
// Hook up a reference to the timer.
#IBOutlet var workoutTimer: WKInterfaceTimer!
// Keep track of the time the workout started.
var workoutStartTime: NSDate?
func startWorkout() {
// To count up use 0.0 or less, otherwise the timer counts down.
workoutTimer.setDate(NSDate(timeIntervalSinceNow: 0.0))
workoutTimer.start()
self.workoutStartTime = NSDate()
}
func stopWorkout() {
workoutTimer.stop()
}
func workoutSecondsElapsed() -> NSTimeInterval? {
// If the timer hasn't been started then return nil
guard let startTime = self.workoutStartTime else {
return nil
}
// Time intervals from past dates are negative, so
// multiply by -1 to get the elapsed time.
return -1.0 * self.startTime.timeIntervalSinceNow
}
Comprehensive blog entry: here.
As of 2021, the (Foundation) Timer object supports a tolerance variable (measured in seconds). Set timer.tolerance = 0.2, and you should get a fire every second (+/- 0.2 seconds). If you are just updating your GUI, the exact time interval isn't that critical, but this should be more reliable than using no tolerance value. You'll need to create the timer separately, and manually add to the run queue such as below... (Swift)
import Foundation
// Set up timer to fire every second
let newTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {timer in
self.timerFired()
}
newTimer.tolerance = 0.2 // For visual updates, 0.2 is close enough
RunLoop.current.add(newTimer, forMode: .common)

In which cases runAction on SKNode does not complete?

Are there any known cases where running an SKAction using runAction does not complete?
I launch several 'runAction' on different SKNode. In order to synchronize all these actions, I use a counter that is incremented inside the completion block of each SKAction. When the counter reach the exact number of launched SKAction then the animations is completed.
From time to time one SKAction does not complete then the animation never complete.
// Several actions are launched...
myNode.runAction(myActions,completion:{
checkCompletion()
})
// Check if all actions completed
//
// numberOfLaunchedActions: number of actions launched
// logDebug: some log helper
func checkCompletion() {
// This counter is initialized earlier
numberOfCompletedActions++
logDebug(">> Actions completed: \(numberOfCompletedActions)/\(numberOfLaunchedActions)")
if numberOfCompletedActions == numberOfLaunchedActions {
/// some statements
logDebug("Animation Completed!")
}
}
Actions are dynamically generated and are composed of sequence of following actions:
waitForDuration
scaleTo
moveBy
hide
unhide
No removeFromParent nor runAction nor runBlock.
The action I focus my attention on is the following:
let waitAction = SKAction.waitForDuration(0.4)
let scaleAction = SKAction.scaleTo(0.1, duration: 2.0)
scaleAction.timingMode = .EaseOut
let myAction = SKAction.sequence([
waitAction,
scaleAction,
])
There is one known case: adding action after Remove from parent in a sequence: SKAction runAction does not execute completion block
As explained in comment:
Remove from parent is causing the rest of the actions in the
sequence not to be called, since the involved node is no longer in
the scene. The sequence didn't complete, therefore the completion
block shouldn't be called.

Delay 'glitch' with dispatch_after swift

Currently, i have a delay function as follows:
//Delay function from http://stackoverflow.com/questions/24034544/dispatch-after-gcd-in-swift/24318861#24318861
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
This code works for what i need, but as soon as the delay gets greater than 13 or so seconds, it seems to glitch out and stop delaying. Does anyone know a solution to this, or even while this is happening?
Here is the code in my use:
var delayTime = Double(1)
for number in self.gameOrder{
if number == 0{
delay(delayTime++){self.greenButton.highlighted = true}
self.delay(delayTime++){
self.greenButton.highlighted = false
}
}
else if number == 1{
delay(delayTime++){self.redButton.highlighted = true}
self.delay(delayTime++){
self.redButton.highlighted = false
}
}
else if number == 2{
delay(delayTime++){self.yellowButton.highlighted = true}
self.delay(delayTime++){
self.yellowButton.highlighted = false
}
}
else if number == 3{
delay(delayTime++){self.blueButton.highlighted = true}
self.delay(delayTime++){
self.blueButton.highlighted = false
}
}
println(delayTime)
}
}
}
Once delayTime gets to 13, the delay starts to play up.
Thanks!
You didn't say what platform/OS, but if on iOS, this behavior changed from iOS 7 to iOS 8. It would appear to be coalescing the timers (a power saving feature to group similar timer events together to minimize the power consumption).
The solution is to refactor the code either to use a single repeating timer or rather than scheduling all of the dispatch_after calls up front, have each dispatch_after trigger the next dispatch_after in its completion block (thus never having a bunch of dispatch_after calls pending at the same time that it might be coalesced together).
By the way, if using a repeating timer, you might want to use a dispatch source timer rather than a NSTimer, as this not only gives you the ability to specify the desired leeway, but the third parameter of dispatch_source_set_timer lets you specify a value of DISPATCH_TIMER_STRICT which:
Specifies that the system should make a best effort to strictly observe the
leeway value specified for the timer via dispatch_source_set_timer(), even
if that value is smaller than the default leeway value that would be applied
to the timer otherwise. A minimal amount of leeway will be applied to the
timer even if this flag is specified.
CAUTION: Use of this flag may override power-saving techniques employed by
the system and cause higher power consumption, so it must be used with care
and only when absolutely necessary.
In Mac OS X, this can be used to turn off "App Nap" feature (where timers will be more significantly altered in order to maximize battery life), but given the appearance of this timer coalescing in iOS, it might be a useful option here, too.