I have an issue with my code which exemplified by a simplified version below. I expect for "2" to be printed before "1". However, that is not the case.
Does this have something to do with the fact that the code doesn't actually run from top to bottom?
if moveConclusion.patternDetected == true {
for i in 0...8 {
if pressedArray[i] {
self.panlButtons[i].backgroundColor = UIColor.clear
UIView.animate(withDuration: 0.1, animations:{
self.panlButtons[i].backgroundColor = self.correctColour
//self.panlButtons[i].transform = CGAffineTransform(rotationAngle: CGFloat.pi / -12)
}, completion: { finished in
UIView.animate(withDuration: 0.1, animations:{
self.panlButtons[i].backgroundColor = UIColor.clear
// self.panlButtons[i].transform = CGAffineTransform(rotationAngle: CGFloat.pi / 12)
print("2")
})
})
}
}
print("1")
}
Output:
1
2
2
2
...
Calling UIView.animate orders the animation; it does not perform it. The animation engine will perform it — later. You are handing to the animation engine two blocks to be executed later, meaning after all your code comes to an end (to be technical, it's when the next screen refresh frame comes along and the current CATransaction is committed):
The animations block will run after all your code has finished and it's time for the animation to start.
The completion block will run after the animation ends (that's what "completion" means).
UIView.animate runs on the main thread and is asynchronous. So as soon as that code is called it dispatches it onto the NEXT runloop, (which is after the this function terminates, which means after the entire for loop).
In other words, the UIView.animate call essentially just queues it for the next run loop. And the print("1") statement still occurs on the current run loop.
Then the animation block is run, and once the animation block finishes, the completion block runs. So the print for the 2 comes WAAAAAY later in terms of CPU processing, and is definitely the expected behavior.
This is because print("2") executes approximately 0.1-0.2 seconds after this code starts, but print("1") happens after the for loop executes.
The animations do not execute synchronously with the for loop. The print("2") will happen during the second animation, but by that time, the for loop has completed and print("1") has executed.
Related
I'm trying to change an UIImageView alpha attribute on click. However, the change of the attribute is done after all the code-lines are run, and I want it to be as soon as the click is done. To simplify the code, I changed all the code-lines for a sleep(2) as it is giving the same effect.
The click function
#IBAction func click_btn() {
imageView.alpha = 0.5
sleep(2)
}
I've tried using DispatchQueue and async methods but the result is the same.
Here's the result, as you can see, when I click the button the Image doesn't change until the 2 sleep seconds pass. How can I change the alpha property immediately after the click?
That is the way the event loop on iOS (and Mac OS) works. UI changes are queued up as you run through your code, and then applied once you return and the app visits the event loop. Your delay is PREVENTING the change from taking place. If you get rid of the sleep it will work as intended.
Note that normally button taps fire on "touch up inside", which means the button doesn't respond until the user releases their finger. You can attach the action to a touch down event instead if you want.
What is it you actually want to accomplish?
Edit:
Rewrite your code like this:
#IBAction func click_btn() {
imageView.alpha = 0.5
DispatchQueue.main.async() {
//Code that takes 2 seconds to run
}
}
That will cause your function to return immediately, and your alpha change to take place. On the next pass through the event loop, the system will find your async task on the main queue, pick it up and start processing it. Note that for the 2 seconds it takes to run, the UI will freeze. It might be better to run the slow code on a background queue and only update the UI once the "slow bits" are finished:
#IBAction func click_btn() {
imageView.alpha = 0.5
DispatchQueue. global().async {
// Run the code that takes 2 seconds to run on a background thread
DispatchQueue.main.async() {
// Do your UI updates that have to run after the slow code finishes on the main thread.
}
}
I'm trying to change an UIImageView alpha attribute on click. However, the change of the attribute is done after all the code-lines are run, and I want it to be as soon as the click is done. To simplify the code, I changed all the code-lines for a sleep(2) as it is giving the same effect.
The click function
#IBAction func click_btn() {
imageView.alpha = 0.5
sleep(2)
}
I've tried using DispatchQueue and async methods but the result is the same.
Here's the result, as you can see, when I click the button the Image doesn't change until the 2 sleep seconds pass. How can I change the alpha property immediately after the click?
That is the way the event loop on iOS (and Mac OS) works. UI changes are queued up as you run through your code, and then applied once you return and the app visits the event loop. Your delay is PREVENTING the change from taking place. If you get rid of the sleep it will work as intended.
Note that normally button taps fire on "touch up inside", which means the button doesn't respond until the user releases their finger. You can attach the action to a touch down event instead if you want.
What is it you actually want to accomplish?
Edit:
Rewrite your code like this:
#IBAction func click_btn() {
imageView.alpha = 0.5
DispatchQueue.main.async() {
//Code that takes 2 seconds to run
}
}
That will cause your function to return immediately, and your alpha change to take place. On the next pass through the event loop, the system will find your async task on the main queue, pick it up and start processing it. Note that for the 2 seconds it takes to run, the UI will freeze. It might be better to run the slow code on a background queue and only update the UI once the "slow bits" are finished:
#IBAction func click_btn() {
imageView.alpha = 0.5
DispatchQueue. global().async {
// Run the code that takes 2 seconds to run on a background thread
DispatchQueue.main.async() {
// Do your UI updates that have to run after the slow code finishes on the main thread.
}
}
I have an animation running with SKAction.repeatForever animation loop, which runs an animation all over again for indefinite amount of time.
node.run(SKAction.repeatForever(
SKAction.animate(with: animationFrames,
timePerFrame: timePerFrame,
resize: true,
restore: true)))
I have a user-triggered (with a touch) animation that I want to fire after the currently animated loop finishes. How can I catch when the current loop of SKAction.animate finishes and then stop, remove and replace the animation with a second one?
|--Loop1--|--Loop2--|--Loop3--|--...
| user trigger here
| wait until Loop2 finishes and then replace the animation
Pseudo code of what kind of functionality I'm looking for:
... somewhere else ... {
animatedNode.run(SKAction.repeatForever(SKAction.animate(...)))
}
func onTouch() {
foreverRunningAnimation.waitUntilLoopFinishes() {
animatedNode.removeAllActions()
animatedNode.run(SKAction.repeatForever(SKAction.animate(< second animation>)))
}
}
I am creating a game where the user can move a SKShapeNode around. Now, I am trying to write a utility function that will perform back-to-back animations sequentially.
Description of what I'm Trying
I first dispatch_async to a serial thread. This thread then calls a dispatch_sync on the main thread to perform an animation. Now, to make the animations run sequentially, I would like to block the GlobalSerialAnimationQueue until the animation is completed on the main thread. By doing this, I (theoretically) would be able to run animations sequentially. My code is pasted below for more description
func moveToCurrentPosition() {
let action = SKAction.moveTo(self.getPositionForCurrRowCol(), duration: 1.0)
dispatch_async(GlobalSerialAnimateQueue) {
//this just creates an action to move to a point
dispatch_sync(GlobalMainQueue, {
self.userNode!.runAction(action) {
//inside the completion block now want to continue
//WOULD WANT TO TRIGGER THREAD TO CONTINUE HERE
}
})
//WOULD LIKE TO PAUSE HERE, THIS BLOCK FINISHING ONLY WHEN THE ANIMATION IS COMPLETE
}
}
So, my question is, how would I write a function that can take in requests for animations, and then perform them sequentially? What grand-central-dispatch tools should I use, or should I be trying a completely different approach?
I figured out how to do this using a grand-central-dispatch semaphore. My updated code is here.
func moveToCurrentPosition() {
let action = SKAction.moveTo(self.getPositionForCurrRowCol(), duration: Animation.USER_MOVE_DURATION)
dispatch_async(GlobalSerialAnimateQueue) {
let semaphore = dispatch_semaphore_create(0)
dispatch_sync(GlobalMainQueue, {
self.userNode!.runAction(action) {
//signal done
dispatch_semaphore_signal(semaphore)
}
})
//wait here...
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
}
SpriteKit provides a much simpler and intuitive API for running actions in sequence. Have a look at the documentation here.
You can simply perform actions as a sequence of events, with blocks in between or as completion:
let action = SKAction.moveTo(self.getPositionForCurrRowCol(), duration: Animation.USER_MOVE_DURATION)
let otherAction = SKAction.runBlock({
//Perform completion here.
})
self.userNode!.runAction(SKAction.sequence([action, otherAction]))
I can't wait for group of SKAction because completion handler executes immediately. Here is basic example:
let a1 = SKAction.runAction(SKAction.fadeAlphaTo(0.5, duration: duration), onChildWithName: child1.name!)
let a2 = SKAction.runAction(SKAction.fadeAlphaTo(1, duration: duration), onChildWithName: child2.name!)
runAction(SKAction.group([a1, a2]), completion: { () -> Void in
// do something
})
Nodes child1 and child2 are children of scene. When I run app on iPhone 5s (iOS 8.4) simulator I see that completion block doesn't wait while group actions finished and starts immediately.
So my two questions:
Is it s bug? If no, where is in SKAction documentation I can find explanation of this behaviour.
How to do this right? I know I can use counter and start each action a1 and a2 with completion block and just check when counter became 0 but it looks like a boilerplate code for me.
According to Apple's documentation, SKAction.runAction returns an action of instantaneous duration (that is 0 duration).
Thus in your code both SKActions, a1 and a2, have instantaneous duration. This is why the completion block is called immediately.
From Apple Documentation
SKAction.runAction : This action has an instantaneous duration,
although the action executed on the child may have a duration of its
own.
https://developer.apple.com/library/ios/documentation/SpriteKit/Reference/SKAction_Ref/#//apple_ref/occ/clm/SKAction/runAction:onChildWithName:
One way to set the completion handler would be to set it to one of the child nodes, since both actions have the same duration.
let a1 = SKAction.fadeAlphaTo(0.5, duration: 1);
let a2 = SKAction.fadeAlphaTo(1, duration: 1);
child1.runAction(a1)
child2.runAction(a2) { () -> Void in
//completion code
}