Playing a "snap in/snap out" animation during zoom in, zoom out - ios

So i got a set of rings one inside the other. I want to be able to zoom then in and, when I zoom close enough the zooming should animate the snap in and then I should keep zooming again.
I've done the zooming part pretty easily, using CGAffineTransform and UIPinchGesture recognizer, here is how the scale method looks like:
func scale(gesture: UIPinchGestureRecognizer) {
if !animationInProgress {
for var i = 0; i < scrollViews.count; i++ {
var view = scrollViews[i]
var j = 0
for ; j < scrollViews.count; j++ {
if view == scrollViews[j] {
break
}
}
println("Animation in progress: \(animationInProgress)")
if view.frame.origin.x < -80 || view.frame.origin.y < -140 {
if !view.hidden {
currentViewIndex = j + 1
view.hidden = true
view.visible = false
println("new view got out of the screen")
snapIn = true
animateSnap(snapIn)
}
} else {
if view.hidden == true {
currentViewIndex = j
view.hidden = false
view.visible = true
println("new view got into the screen")
snapIn = false
animateSnap(snapIn)
}
}
view.transform = CGAffineTransformScale(view.transform, gesture.scale, gesture.scale)
println("transformed")
}
gesture.scale = 1
}
}
so we iterate through our views, the frame.origin x and y checks, whether the views is out of the screen and I set some flags, which turn of some calculations.
The idea is the following, when one circle gets out of the screen, the rest animate zoom in and zoom out, when the new view gets on the screen.
Here is a animateSnap function:
private func animateSnap(snapIn: Bool) {
let factor: CGFloat = snapIn ? 1.5 : 0.5
for var a = currentViewIndex; a < scrollViews.count; a++ {
let next = scrollViews[a]
var transformAnimation = CABasicAnimation(keyPath: "transform")
transformAnimation.duration = 1
transformAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
transformAnimation.removedOnCompletion = false
transformAnimation.fillMode = kCAFillModeForwards
transformAnimation.delegate = self
let transform = CATransform3DScale(next.layer.transform, factor, factor, 1)
transformAnimation.toValue = NSValue(CATransform3D:transform)
next.layer.addAnimation(transformAnimation, forKey: "transform")
}
}
The question is: does somebody know an elegant solution to this or see any flaw in my approach ? The problem happens when the animation ends the views kinda go back to where they were at the start. I don't know why because remove on completion is set to false. In addition, can the fact that I use CGAffineTransform for zooming and CATransform3D somehow affect the result ?
Thank you very much !

For everybody who might encounter the same problem, when you work with 2D use CGAffineTransform instead of CATransform3D. That solved the problem, in addition I suspect that they also change different things(for example, CATransform3D changes layer) that's why when animation finished the result did not look as expected.

Related

problem with image animation in viewDidAppear(), any advice?

I am trying to create an animation that scrolls through a series of images. The following code does not have any animation and only the last image of the series appears on the screen. How do I fix this animation?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
imageSelection(score: 50)
}
func imageSelection(score: Int) {
var myScore = score
if score < 10 {
myScore = 1
}
else if score < 20 && score > 10 {
myScore = 2
}
else if score < 30 && score > 20 {
myScore = 3
}
else if score < 40 && score > 30 {
myScore = 4
}
else if score < 50 && score > 40 {
myScore = 5
}
else if score == 50 {
myScore = 6
}
for i in 1...myScore{
UIView.animate(withDuration: 0.5) {
self.cookieImage.image = UIImage(named: "rewardsCookie\(i)")
self.view.layoutIfNeeded()
}
}
}
As Matt said, you are not animating any animatable properties. You can achieve what you want to by using layer animations. Replace your for loop with the following code.
var images = [CGImage]()
for i in 1...myScore{
images.append(UIImage(named: "rewardsCookie\(i)")!.cgImage!)
}
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.values = images
let delay = 3.0 // in case you need to delay your animation
animation.duration = 5.0 // change to the total duration (ex: 0.5 * myScore)
animation.beginTime = CACurrentMediaTime() + delay
self.cookieImage.layer.add(animation, forKey: nil)
Note that for layer animations, you are not actually seeing the UIImageView, but a cached version of it. This is removed from the screen once the animation completes and the original layer shows itself again. So you will see the image that was visible on the cookieImageView before the animation began. In order to persist the last image, add the following line after the code above
self.cookieImage.image = UIImage(named: "rewardsCookie\(myScore)")
For more information go through Apple's Documentation.
Are you trying do a "flip book" animation where you flip between a sequence of images? The easy way to do that is with UIImageView animation. See the UIImageView class reference in Xcode for more information (see the section titled "Animating a Sequence of Images".

'Press and hold' animation without NSTimer

Update: The NSTimer approach works now, but comes with a huge performance hit. The question is now narrowed down to an approach without NSTimers.
I'm trying to animate a 'Press and hold' interactive animation. After following a load of SO answers, I've mostly followed the approach in Controlling Animation Timing by #david-rönnqvist. And it works, if I use a Slider to pass the layer.timeOffset.
However, I can't seem to find a good way to continuously update the same animation on a press and hold gesture. The animation either doesn't start, only shows the beginning frame or at some points finishes and refuses to start again.
Can anyone help with achieving the following effect, without the horrible NSTimer approach I'm currently experimenting with?
On user press, animation starts, circle fills up.
While user holds (not necessarily moving the finger), the animation should continue until the end and stay on that frame.
When user lifts finger, the animation should reverse, so the circle is empties again.
If the user lifts his finger during the animation or presses down again during the reverse, the animation should respond accordingly and either fill or empty from the current frame.
Here's a Github repo with my current efforts.
As mentioned, the following code works well. It's triggered by a slider and does its job great.
func animationTimeOffsetToPercentage(percentage: Double) {
if fillShapeLayer == nil {
fillShapeLayer = constructFillShapeLayer()
}
guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else {
print("Animation not found")
return
}
let timeOffset = maximumDuration * percentage
print("Set animation to percentage \(percentage) with timeOffset: \(timeOffset)")
fillAnimationLayer.timeOffset = timeOffset
}
However, the following approach with NSTimers works, but has an incredible performance hit. I'm looking for an approach which doesn't use the NSTimer.
func beginAnimation() {
if fillShapeLayer == nil {
fillShapeLayer = constructFillShapeLayer()
}
animationTimer?.invalidate()
animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in
if self.layer.timeOffset >= 1.0 {
self.layer.timeOffset = self.maximumDuration
}
else {
self.layer.timeOffset += 0.1
}
})
}
func reverseAnimation() {
guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else {
print("Animation not found")
return
}
animationTimer?.invalidate()
animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in
if self.layer.timeOffset <= 0.0 {
self.layer.timeOffset = 0.0
}
else {
self.layer.timeOffset -= 0.1
}
})
}
When you use slider you use fillAnimationLayer layer for animation
fillAnimationLayer.timeOffset = timeOffset
However, in beginAnimation and reverseAnimation functions you are using self.layer.
Try to replace self.layer.timeOffset with self.fillShapeLayer!.timeOffset in your timer blocks.
The solution is two-fold;
Make sure the animation doesn't remove itself on completion and keeps its final frame. Easily accomplished with the following lines of code;
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
The hard part; you have to remove the original animation and start a new, fresh reverse animation that begins at the correct point. Doing this, gives me the following code;
func setAnimation(layer: CAShapeLayer, startPath: AnyObject, endPath: AnyObject, duration: Double)
{
// Always create a new animation.
let animation: CABasicAnimation = CABasicAnimation(keyPath: "path")
if let currentAnimation = layer.animationForKey("animation") as? CABasicAnimation {
// If an animation exists, reverse it.
animation.fromValue = currentAnimation.toValue
animation.toValue = currentAnimation.fromValue
let pauseTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
// For the timeSinceStart, we take the minimum from the duration or the time passed.
// If not, holding the animation longer than its duration would cause a delay in the reverse animation.
let timeSinceStart = min(pauseTime - startTime, currentAnimation.duration)
// Now convert for the reverse animation.
let reversePauseTime = currentAnimation.duration - timeSinceStart
animation.beginTime = pauseTime - reversePauseTime
// Remove the old animation
layer.removeAnimationForKey("animation")
// Reset startTime, to be when the reverse WOULD HAVE started.
startTime = animation.beginTime
}
else {
// This happens when there is no current animation happening.
startTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
animation.fromValue = startPath
animation.toValue = endPath
}
animation.duration = duration
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
layer.addAnimation(animation, forKey: "animation")
}
This Apple article explains how to do a proper pause and resume animation, which is converted to use with the reverse animation.

Gradually increasing speed of scrolling background in SpriteKit

I am making a simple game in SpriteKit, and I have a scrolling background. What simply happens is that a few background images are placed adjacent to each other when the game scene is loaded, and then the image is moved horizontally when it scrolls out of the screen. Here is the code for that, from my game scene's didMoveToView method.
// self.gameSpeed is 1.0 and gradually increases during the game
let backgroundTexture = SKTexture(imageNamed: "Background")
var moveBackground = SKAction.moveByX(-self.frame.size.width, y: 0, duration: (20 / self.gameSpeed))
var replaceBackground = SKAction.moveByX(self.frame.size.width, y: 0, duration: 0)
var moveBackgroundForever = SKAction.repeatActionForever(SKAction.sequence([moveBackground, replaceBackground]))
for var i:CGFloat = 0; i < 2; i++ {
var background = SKSpriteNode(texture: backgroundTexture)
background.position = CGPoint(x: self.frame.size.width / 2 + self.frame.size.width * i, y: CGRectGetMidY(self.frame))
background.size = self.frame.size
background.zPosition = -100
background.runAction(moveBackgroundForever)
self.addChild(background)
}
Now I want to increase the speed of the scrolling background at certain points of the game. You can see that the duration of the background's horizontal scroll is set to (20 / self.gameSpeed). Obviously this does not work, because this code is only run once, and therefore the movement speed is never updated to account for a new value of the self.gameSpeed variable.
So, my question is simply: how do I increase the speed (reduce the duration) of my background images' movements according to the self.gameSpeed variable?
Thanks!
You could use the gameSpeed variable to set the velocity of the background. For this to work, firstly, you need to have a reference to your two background pieces (or more if you so wanted):
class GameScene: SKScene {
lazy var backgroundPieces: [SKSpriteNode] = [SKSpriteNode(imageNamed: "Background"),
SKSpriteNode(imageNamed: "Background")]
// ...
}
Now you need your gameSpeed variable:
var gameSpeed: CGFloat = 0.0 {
// Using a property observer means you can easily update the speed of the
// background just by setting gameSpeed.
didSet {
for background in backgroundPieces {
// Minus, because the background is moving from left to right.
background.physicsBody!.velocity.dx = -gameSpeed
}
}
}
Then position each piece correctly in didMoveToView. Also, for this method to work each background piece needs a physics body so you can easily change its velocity.
override func didMoveToView(view: SKView) {
for (index, background) in enumerate(backgroundPieces) {
// Setup the position, zPosition, size, etc...
background.physicsBody = SKPhysicsBody(rectangleOfSize: background.size)
background.physicsBody!.affectedByGravity = false
background.physicsBody!.linearDamping = 0
background.physicsBody!.friction = 0
self.addChild(background)
}
// If you wanted to give the background and initial speed,
// here's the place to do it.
gameSpeed = 1.0
}
You could update gameSpeed in update for example with gameSpeed += 0.5.
Finally, in update you need to check if a background piece has gone offscreen (to the left). If it has it needs to be moved to the end of the chain of background pieces:
override func update(currentTime: CFTimeInterval) {
for background in backgroundPieces {
if background.frame.maxX <= 0 {
let maxX = maxElement(backgroundPieces.map { $0.frame.maxX })
// I'm assuming the anchor of the background is (0.5, 0.5)
background.position.x = maxX + background.size.width / 2
}
}
}
You could make use of something like this
SKAction.waitforDuration(a certain amount of period to check for the updated values)
SKAction.repeatActionForever(the action above)
runAction(your action)
{ // this is the completion block, do whatever you want here, check the values and adjust them accordly
}

Zoom SKSpriteNode in AND out in Swift

First off, I have already seen and tried to implement the other answers to similar questions here, here and here. The problem is I started programming for iOS last year with Swift and (regrettably) I did not learn ObjC first (yes, it's now on my to-do list). ;-)
So please take a look and see if you might help me see my way thru this.
I can easily pinch to zoom the whole SKScene. I can also scale an SKSpiteNode up/down by using other UI Gestures (ie. swipes) and SKActions.
Based off this post I have applied the SKAction to the UIPinchGestureRecognizer and it works perfectly to zoom IN, but I cannot get it to zoom back OUT.
What am I missing?
Here is my code on a sample project:
class GameScene: SKScene {
var board = SKSpriteNode(color: SKColor.yellowColor(), size: CGSizeMake(200, 200))
func pinched(sender:UIPinchGestureRecognizer){
println("pinched \(sender)")
// the line below scales the entire scene
//sender.view!.transform = CGAffineTransformScale(sender.view!.transform, sender.scale, sender.scale)
sender.scale = 1.01
// line below scales just the SKSpriteNode
// But it has no effect unless I increase the scaling to >1
var zoomBoard = SKAction.scaleBy(sender.scale, duration: 0)
board.runAction(zoomBoard)
}
// line below scales just the SKSpriteNode
func swipedUp(sender:UISwipeGestureRecognizer){
println("swiped up")
var zoomBoard = SKAction.scaleBy(1.1, duration: 0)
board.runAction(zoomBoard)
}
// I thought perhaps the line below would scale down the SKSpriteNode
// But it has no effect at all
func swipedDown(sender:UISwipeGestureRecognizer){
println("swiped down")
var zoomBoard = SKAction.scaleBy(0.9, duration: 0)
board.runAction(zoomBoard)
}
override func didMoveToView(view: SKView) {
self.addChild(board)
let pinch:UIPinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: Selector("pinched:"))
view.addGestureRecognizer(pinch)
let swipeUp:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedUp:"))
swipeUp.direction = .Up
view.addGestureRecognizer(swipeUp)
let swipeDown:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedDown:"))
swipeDown.direction = .Down
view.addGestureRecognizer(swipeDown)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
// should I be using this function instead?
}
Thanks to the help from #sangony I have gotten this working finally. I thought I'd post the working code in case anyone else would like to see it in Swift.
var board = SKSpriteNode(color: SKColor.yellowColor(), size: CGSizeMake(200, 200))
var previousScale = CGFloat(1.0)
func pinched(sender:UIPinchGestureRecognizer){
if sender.scale > previousScale {
previousScale = sender.scale
if(board.size.height < 800) {
var zoomIn = SKAction.scaleBy(1.05, duration:0)
board.runAction(zoomIn)
}
}
if sender.scale < previousScale {
previousScale = sender.scale
if(board.size.height > 200) {
var zoomOut = SKAction.scaleBy(0.95, duration:0)
board.runAction(zoomOut)
}
}
I tried your code (in Objective C) and got it to zoom in and out using pinch. I don't think there's anything wrong with your code but you are probably not taking into account the scale factors as they are placed on the ever changing sprite size.
You can easily zoom so far out or in that it requires multiple pinch gestures to get the node back to a manageable size. Instead of using the scale property directly for your zoom factor, I suggest you use a step process. You should also have max/min limits for your scale size.
To use the step process you create a CGFloat ivar previousScale to store the last scale value as to determine whether the current pinch is zooming in or out. You then compare the new passed sender.scale to the ivar and zoom in or out based on the comparison.
Apply min and max scale limits to stop scaling once they are reached.
The code below is in Obj-C but I'm sure you can get the gist of it:
First declare your ivar float float previousScale;
- (void)handlePinch:(UIPinchGestureRecognizer *)sender {
NSLog(#"pinchScale:%f",sender.scale);
if(sender.scale > previousScale) {
previousScale = sender.scale;
// only scale up if the node height is less than 200
if(node0.size.height < 200) {
// step up the scale factor by 0.05
[node0 runAction:[SKAction scaleBy:1.05 duration:0]];
}
}
if(sender.scale < previousScale) {
previousScale = sender.scale;
// only scale down if the node height is greater than 20
if(node0.size.height > 20) {
// step down the scale factor by 0.05
[node0 runAction:[SKAction scaleBy:0.95 duration:0]];
}
}
}

Animation not working when use POPDecayAnimation

I make dropping down animation for changing views. When I use POPSpringAnimation all working correctly. But when I change POPSpringAnimation to POPDecayAnimation nothing happens and in debug output I see this message:
ignoring to value on decay animation <POPDecayAnimation:0x7f9f2b5c8700; from = null; to = null; deceleration = 0.998000>
Code:
func switchViewController(from: UIViewController?, toViewController to: UIViewController?, animated: Bool) {
if animated {
to!.view.frame = self.view.frame
// Not working when call trulySwitchViewController
self.addChildViewController(to!)
self.view.insertSubview(to!.view, belowSubview: from!.view)
to!.didMoveToParentViewController(self)
// Calculate new x coord
var frame = from!.view.frame
frame.origin.y += frame.size.height
// POPDecayAnimation not working here
var animation = POPDecayAnimation()
animation.property = POPAnimatableProperty.propertyWithName(kPOPViewFrame) as POPAnimatableProperty
animation.toValue = NSValue(CGRect:frame)
animation.completionBlock = {
(_, _) in
self.trulySwitchViewController(from, toViewController: nil)
}
from!.view.pop_addAnimation(animation, forKey: "dropDown")
} else {
to?.view.frame = self.view.frame
self.trulySwitchViewController(from, toViewController: to)
}
}
For a decay animation you need to set the velocity property, this then decays to zero based on the deceleration property. So, the to property is not needed, the end state is determined based on the start position, velocity and deceleration.
You also need to animate the layer position not the view frame. So something like this should work...
var animation = POPDecayAnimation()
animation.property = POPAnimatableProperty.propertyWithName(kPOPLayerPosition) as POPAnimatableProperty
from!.view.layer.pop_addAnimation(animation, forKey: "dropDown")

Resources