How do I lock and unlock a critical section of code in a UIView animation completion block using Swift (in a subclassed UIView)?
func MoveCard(sourcePile: Pile, destPile: Pile) {
// Temporarily disable user interaction
disableUserInteraction()
// Move card from source pile to destination pile
UIView.animateWithDuration(0.1) {
() -> Void in
// Move card center
self.center = destPile.center
// CRITICAL SECTION OF CODE
// Add card to destination pile array
destPile.cards.append(sourcePile.cards.last!)
// Remove card from source pile array
sourcePile.cards.removeLast()
// Reenable user interaction
enableUserInteraction()
}
}
Try this:
UIView.animateWithDuration(0.1, animations: {
// your animation code
}, completion: { (complete: Bool) in
// your completion code
})
Related
Desired behavior is: when an action is removed from a node (with removeAction(forKey:) for instance) it stops to animate and all the changes caused by action are discarded, so the node returns back to pervious state. In other words, I want to achieve behavior similar to CAAnimation.
But when a SKAction is removed, the node remains changed. It's not good, because to restore it's state I need to know exactly what action was removed. And if I then change the action, I also will need to update the node state restoration.
Update:
The particular purpose is to show possible move in a match-3 game. When I show a move, pieces start pulsating (scale action, repeating forever). And when the user moves I want to stop showing the move, so I remove the action. As the result, pieces may remain downscaled. Later I would like to add more fancy and complicated animations, so I want to be able to edit it easily.
Thanks to the helpful comment and answer I came to my own solution. I think the state machine would be bit too heavy here. Instead I created a wrapper node, which main purpose is run the animation. It also has a state: isAimating property. But, first of all, it allows to keep startAnimating() and stopAnimating() methods close to each other, incapsulated, so it's more difficult to mess up.
class ShowMoveAnimNode: SKNode {
let animKey = "showMove"
var isAnimating: Bool = false {
didSet {
guard oldValue != isAnimating else { return }
if isAnimating {
startAnimating()
} else {
stopAnimating()
}
}
}
private func startAnimating() {
let shortPeriod = 0.2
let scaleDown = SKAction.scale(by: 0.75, duration: shortPeriod)
let seq = SKAction.sequence([scaleDown,
scaleDown.reversed(),
scaleDown,
scaleDown.reversed(),
SKAction.wait(forDuration: shortPeriod * 6)])
let repeated = SKAction.repeatForever(seq)
run(repeated, withKey: animKey)
}
private func stopAnimating() {
removeAction(forKey: animKey)
xScale = 1
yScale = 1
}
}
Usage: just add everything that should be animated to this node. Works well with simple animations, like: fade, scale and move.
As #Knight0fDragon suggested, you would be better off using the GKStateMachine functionality, I will give you an example.
First declare the states of your player/character in your scene
lazy var playerState: GKStateMachine = GKStateMachine(states: [
Idle(scene: self),
Run(scene: self)
])
Then you need to create a class for each of these states, in this example I will show you only the Idle class
import SpriteKit
import GameplayKit
class Idle: GKState {
weak var scene: GameScene?
init(scene: SKScene) {
self.scene = scene as? GameScene
super.init()
}
override func didEnter(from previousState: GKState?) {
//Here you can make changes to your character when it enters this state, for example, change his texture.
}
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass is Run.Type //This is pretty obvious by the method name, which states can the character go to from this state.
}
override func update(deltaTime seconds: TimeInterval) {
//Here is the update method for this state, lets say you have a button which controls your character velocity, then you can check if the player go over a certain velocity you make it go to the Run state.
if playerVelocity > 500 { //playerVelocity is just an example of a variable to check the player velocity.
scene?.playerState.enter(Run.self)
}
}
}
Now of course in your scene you need to do two things, first is initialize the character to a certain state or else it will remain stateless, so you can to this in the didMove method.
override func didMove(to view: SKView) {
playerState.enter(Idle.self)
}
And last but no least is make sure the scene update method calls the state update method.
override func update(_ currentTime: TimeInterval) {
playerState.update(deltaTime: currentTime)
}
I'm using UIViewPropertyAnimator to run an array interactive animations, and one issue I'm having is that whenever the I reverse the animations I can't run the animations back forward again.
I'm using three functions to handle the animations in conjunction with a pan gesture recognizer.
private var runningAnimations = [UIViewPropertyAnimator]()
private func startInteractiveTransition(gestureRecognizer: UIPanGestureRecognizer, state: ForegroundState, duration: TimeInterval) {
if runningAnimations.isEmpty {
animateTransitionIfNeeded(gestureRecognizer: gestureRecognizer, state: state, duration: duration)
}
for animator in runningAnimations {
animator.pauseAnimation()
animationProgressWhenInterrupted = animator.fractionComplete
}
}
private func animateTransitionIfNeeded(gestureRecognizer: UIPanGestureRecognizer, state: ForegroundState, duration: TimeInterval) {
guard runningAnimations.isEmpty else {
return
}
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
switch state {
case .expanded:
// change frame
case .collapsed:
// change frame
}
}
frameAnimator.isReversed = false
frameAnimator.addCompletion { _ in
print("remove all animations")
self.runningAnimations.removeAll()
}
self.runningAnimations.append(frameAnimator)
for animator in runningAnimations {
animator.startAnimation()
}
}
private func updateInteractiveTransition(gestureRecognizer: UIPanGestureRecognizer, fractionComplete: CGFloat) {
if runningAnimations.isEmpty {
print("empty")
}
for animator in runningAnimations {
animator.fractionComplete = fractionComplete + animationProgressWhenInterrupted
}
}
What I've noticed is after I reverse the animations and then call animateTransitionIfNeeded, frameAnimator is appended to running animations however when I call updateInteractiveTransition immediately after and check runningAnimations, it's empty.
So I'm led to believe that this may have to do with how swift handles memory possibly or how UIViewAnimating completes animations.
Any suggestions?
I've come to realize the issue I was having the result of how UIViewPropertyAnimator handles layout constraints upon reversal.
I couldn't find much detail on it online or in the official documentation, but I did find this which helped a lot.
Animator just animates views into new frames. However, reversed or not, the new constraints still hold regardless of whether you reversed the animator or not. Therefore after the animator finishes, if later autolayout again lays out views, I would expect the views to go into places set by currently active constraints. Simply said: The animator animates frame changes, but not constraints themselves. That means reversing animator reverses frames, but it does not reverse constraints - as soon as autolayout does another layout cycle, they will be again applied.
Like normal you set your constraints and call view.layoutIfNeeded()
animator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
[unowned self] in
switch state {
case .expanded:
self.constraintA.isActive = false
self.constraintB.isActive = true
self.view.layoutIfNeeded()
case .collapsed:
self.constraintB.isActive = false
self.constraintA.isActive = true
self.view.layoutIfNeeded()
}
}
And now, since our animator has the ability to reverse, we add a completion handler to ensure that the correct constraints are active upon completion by using the finishing position.
animator.addCompletion { [weak self] (position) in
if position == .start {
switch state {
case .collapsed:
self?.constraintA.isActive = false
self?.constraintB.isActive = true
self?.view.layoutIfNeeded()
case .expanded:
self?.constraintA.isActive = false
self?.constraintB.isActive = true
self?.view.layoutIfNeeded()
}
}
}
The animator operates on animatable properties of views, such as the frame, center, alpha, and transform properties, creating the needed animations from the blocks you provide.
This is the crucial part of the documentation.
You can properly animate:
frame, center, alpha and transform, so you would not be able to animate properly NSConstraints.
You should modify frames of views inside of addAnimations block
Hello guys i have made the first Level of my game, but always when i go from the main menu screen to the first level the screen freezes for like 2 Seconds and the transition from the Main screen to the game is very delayed and laggy and it sometimes doesn't even show up. Is there a way to preload the Scene in the background to prevent the lag?
you can load the resources for the scene in a different thread. I do this in my game to get really snappy scene transitions despite the fact im loading tons of resources.
make a static function in your scene class to preload your scene
class func createResources(withCompletion: (scene: BaseScene) -> ()){
// load resources on other thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {
let scene = YourScene()
// callback on main thread
dispatch_async(dispatch_get_main_queue(), {
// Call the completion handler back on the main queue.
withCompletion(scene: scene)
});
})
}
call it like this
YourScene.createResources(withCompletion: {
[weak self]
scene in
self!.skView.presentScene(scene)
})
So the way to use this is to build your scene in advance on the different thread. since its running on a different thread you shouldnt get that awkward pause.
for example. lets say the player reaches the goal of beating the level. before I was using this method the game would pause for a second before loading the next scene.
When the player beats the level now I still allow them to move around until the next scene has loaded and then the player will instantly shoot into the next level creating an instant transition.
you can see it here when the ship is hyperspacing between levels. there are a lot of resources loading but the transitions are seamless.
https://www.youtube.com/watch?v=u_bXA3woOmo
Swift 5 version of #hamobi answer
file: DispatchQueueExtensions.swift
import Foundation
extension DispatchQueue {
static func background(_ task: #escaping () -> Void) {
DispatchQueue.global(qos: .background).async {
task()
}
}
static func main(_ task: #escaping () -> Void) {
DispatchQueue.main.async {
task()
}
}
}
file: GameScene.swift
extension GameScene {
class func create(completion: #escaping (_ scene: GameScene) -> Void) {
DispatchQueue.background {
let scene = GameScene()
DispatchQueue.main {
completion(scene)
}
}
}
}
Usage:
GameScene.create(completion: { [weak self] scene in
let transition = SKTransition.doorway(withDuration: 1.0)
self?.view?.presentScene(scene, transition: transition)
})
I'm trying to pre-load an array of texture atlases during which a UIActivityIndicator is displayed. Once the textures are loaded I want to stop the activity indicator using the .stopAnimating() method. I've inserted breakpoints and found that the compiler does get to the .stopAnimating() method, but nothing happens...the indicator continues...
What am I doing wrong here?
class Menu: SKScene {
var activityInd: UIActivityIndicatorView!
override func didMoveToView(view: SKView) {
activityInd = UIActivityIndicatorView(activityIndicatorStyle: .WhiteLarge)
activityInd.center = CGPointMake(self.frame.midX, self.frame.midY)
activityInd.startAnimating()
scene!.view?.addSubview(self.activityInd)
SKTextureAtlas.preloadTextureAtlases([saxAtlas, saxIdleAtlas, drumAtlas, drumIdleAtlas, pianoAtlas, pianoIdleAtlas, bassAtlas]) { () -> Void in
self.activityInd.stopAnimating()
}
}
Usually when you want to stop an activity indicator you just call removeFromSuperview() as it's no good to have a static activity indicator sitting there doing nothing, and that's all that stopAnimating() gives you.
You should also be calling this method on the main thread, since preloadTextureAtlases is a background task and just about anything prefixed with 'UI' needs to be run on the main thread.
SKTextureAtlas.preloadTextureAtlases([saxAtlas, saxIdleAtlas, drumAtlas, drumIdleAtlas, pianoAtlas, pianoIdleAtlas, bassAtlas]) { () -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.activityInd.stopAnimating()
}
}
I followed this awesome Rey Wenderlich tutorial to make an Bezier arc and increment/decrement values. But how to animate the arch instead of just step-up and step-down?
http://www.raywenderlich.com/90690/modern-core-graphics-with-swift-part-1
I tried putting animation block in custom property declaration, which I dont think is the right place to do it and xcode doesn't let me do it anyway.
#IBInspectable var counter: Int = 5 {
didSet {
if counter <= NoOfGlasses {
//the view needs to be refreshed
UIView.animateWithDuration(0.2, animations: {
setNeedsDisplay()
}, completion:nil
)
}
}
}
Also tried to put the increment in animation block in View controller, didn't work.
#IBAction func btnPushButton(sender: AnyObject) {
UIView.animateWithDuration(0.2, animations: {
self.arcView.counter = self.arcView.counter + 10
self.counterLabel.text = String(self.arcView.counter)
}, completion:nil
)
}
Are you describing this sort of thing?
That's a simpler example - it's just a drawn triangle - but it's the same idea, if I'm understanding you correctly: we are animating the difference between one drawing and another.
Basically you have two choices. The easy way is to use CAShapeLayer, which animates for you automatically when you change its path. The other choice is to do what I'm doing here, which is to create a custom animatable property - in this case, a property representing the x-position of the bottom point of the triangle.