Smooth Animation Troubles - ios

I have a button that I am trying to animate to signify sort of a drop down menu effect. Similar to what is seen here
https://medium.com/#phillfarrugia/building-a-tinder-esque-card-interface-5afa63c6d3db
I have used the CGAffineTransform property and converted the degrees to radians to properly rotated. However, the problem comes when I rotate it back the other way. Instead of going back the direction it came it just does somewhat of a akward flip back into the same position.
Could anyone help me rrecreate this smoother transition that is seen in the link I provided
Here is my current code.
#objc func showCalendarPressed(){
print("show calendar pressed")
//will check if there has been any animation work done on the UIVIEW
if self.showCalendar.transform == .identity {
UIView.animate(withDuration: 1.5) {
self.showCalendar.transform = CGAffineTransform(rotationAngle: self.radians(degrees: 180) )
}
}else {
//there is a transform on it and we need to change it back
UIView.animate(withDuration: 0.5, animations: {
//will remove the transform and animate it back
self.showCalendar.transform = .identity
}) { (true) in
}
}
}
Radians Function
func radians(degrees: Double) -> CGFloat{
return CGFloat(degrees * .pi / 180)
}

You are going exactly 1/2 way around (maybe slightly more depending on your conversion to radians), and when you return to .identity, it keeps going the same direction because that is closer (the direction with least rotation). Try using a slightly smaller angle initially. I found this to work:
Replace:
self.radians(degrees: 180)
with:
self.radians(degrees: 179.999)
By going just slightly less than 1/2 way around (which will be imperceptible), returning to .identity will rotate the direction it came from.

Related

Why does CGAffineTransform(scaleX:y:) cause my view to rotate as if I'd called CGAffineTransform(rotationAngle:)?

I took Paul Hudson's (Hacking with Swift) animation tutorial (His Project 15) and tried to extend it to see the effect of multiple animations layered on one another.
There are four distinct animations: scale, translate, rotate and color. Instead of a single button that cycles through all of them, as in his tutorial, I used four buttons to allow selection of each animation individually. I also modified his "undo" animation by reversing the original animation rather than using CGAffineTransform.identity, since the identity transform would undo all the animations to that point.
My problem is that when I click my c button, it does the appropriate scaling the first click but, rather than scale the penguin back to its original size on the second click, it rotates the view. Subsequent clicks of the scale button continue to rotate the view as if I were clicking the rotate button.
There are other anomalies, such as the first click of the move button moves the penguin appropriately but following that with a click of the rotate button, both rotates the penguin and moves it back to the original position. I'm not sure why it moves back but I accept that that might be my own ignorance of the animation system.
I've added print statements and put in breakpoints to debug the scale problem. Everything in the code seems to be working exactly as coded but the animations defy logic! Any help would be appreciated.
The complete program is relatively simple:
import UIKit
class ViewController: UIViewController {
var imageView: UIImageView!
var scaled = false
var moved = false
var rotated = false
var colored = false
#IBOutlet var scaleButton: UIButton!
#IBOutlet var moveButton: UIButton!
#IBOutlet var rotateButton: UIButton!
#IBOutlet var colorButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
imageView = UIImageView(image: UIImage(named: "penguin"))
imageView.center = CGPoint(x: 200, y: 332)
view.addSubview(imageView)
}
#IBAction func tapped(_ sender: UIButton) {
sender.isHidden = true
var theAnimation: ()->Void
switch sender {
case scaleButton:
theAnimation = scaleIt
case moveButton:
theAnimation = moveIt
case rotateButton:
theAnimation = rotateIt
case colorButton:
theAnimation = colorIt
default:
theAnimation = { self.imageView.transform = .identity }
}
print("scaled = \(scaled), moved = \(moved), rotated = \(rotated), colored = \(colored)")
UIView.animate(withDuration: 1, delay: 0, options: [], animations: theAnimation) { finished in
sender.isHidden = false
}
}
func scaleIt() {
print("scaleIt()")
let newScale: CGFloat = self.scaled ? -1.5 : 1.5
self.imageView.transform = CGAffineTransform(scaleX: newScale, y: newScale)
self.scaled.toggle()
}
func moveIt() {
print("moveIt()")
let newX: CGFloat = self.moved ? 0 : -50
let newY: CGFloat = self.moved ? 0 : -150
self.imageView.transform = CGAffineTransform(translationX: newX, y: newY)
self.moved.toggle()
}
func rotateIt() {
print("rotateIt()")
let newAngle = self.rotated ? 0.0 : CGFloat.pi
self.imageView.transform = CGAffineTransform(rotationAngle: newAngle)
self.rotated.toggle()
}
func colorIt() {
print("colorIt()")
let newAlpha: CGFloat = self.colored ? 1 : 0.1
let newColor = self.colored ? UIColor.clear : UIColor.green
self.imageView.alpha = newAlpha
self.imageView.backgroundColor = newColor
self.colored.toggle()
}
}
By any combination of clicking the buttons, I can only arrive at 10 configurations of the Penguin (five with CGAffineTransforms and the same five modified by the color button).
I'll use these images to answer some of your questions. I've tried to label these images "Configuration1A, Configuration2A, ... Configuration1B", where the B configurations are identical to the A configurations but with the color button's effect. Perhaps the labels will show up in my post (I'm very new to posting on StackOverflow).
Here are the five configurations with the first one repeated using the color button:
I first tried repeated button pressings. For each series of pressing a given button, I first returned the penguin to its original position, size, angle and color. For these repeated button pressings, the behavior I observed was as follows:
Button Observed behavior
scale Penguin scales from Configuration1A to Configuration2A (as expected).
scale Penguin rotates by pi from Configuration2A to Configuration4A (WHAT???).
scale Penguin rotates by pi from Configuration4A to Configuration2A (WHAT???).
scale ... steps 2 and 3, above, repeat indefinitely (WHAT???).
move Penguin moves (-50, -150), Configuration1A to Configuration3A (as expected).
move Penguin moves (50, 150) Configuration3A to Configuration1A (as expected).
move ... behavior above repeats indefinitely (as expected).
rotate Penguin rotates by pi, Configuration1A to Configuration5A (as expected).
rotate Penguin rotates by pi, Configuration5A to Configuration1A (as expected).
rotate ... behavior above repeats indefinitely (as expected).
color Penguin's color changes, Configuration1A to Configuration1B (as expected).
color Penguin's color changes, Configuration1B to Configuration1A (as expected).
color ... behavior above repeats indefinitely (as expected).
The opposite of scale 1.5 (50% bigger) is not -1.5, it's 0.5. (50% of it's original size)
Actually, wouldn't you want it to alternate between a scale of 1.5 (50% bigger) and 1.0 (normal size?)
Assuming you do want to alternate between 50% bigger and 50% smaller, change your scaleIt function to:
func scaleIt() {
print("scaleIt()")
let newScale: CGFloat = self.scaled ? -1.5 : 0.5
self.imageView.transform = CGAffineTransform(scaleX: newScale, y: newScale)
self.scaled.toggle()
}
When you set the X or Y scale to a negative number, it inverts the coordinates in that dimension. Inverting in both dimensions will appear like a rotation.
As somebody else mentioned, the way you've written your functions, you won't be able to combine the different transforms. You might want to rewrite your code to apply the changes to the view's existing transform:
func scaleIt() {
print("scaleIt()")
// Edited to correct the math if you are applying a scale to an existing transform
let scaleAdjustment: CGFloat = self.scaled ? -1.5 : 0.66666
self.imageView.transform = self.imageView.transform.scaledBy(x: scaleAdjustment, y: scaleAdjustment)
self.scaled.toggle()
}
Edit:
Note that changes to transforms are not "commutative", which is a fancy way of saying that the order in which you apply them matters. Applying a shift, then a rotate, will give different results than applying a rotate, then a shift, for example.
Edit #2:
Another thing:
The way you've written your functions, they will set the view to its current state, and then invert that current state for next time (you toggle scaled after deciding what to do.)
That means that for function like moveIt() nothing will happen the first time you tap the button. Rewrite that function like this:
func moveIt() {
print("I like to moveIt moveIt!")
self.moved.toggle() //Toggle the Bool first
let newX: CGFloat = self.moved ? 0 : -50
let newY: CGFloat = self.moved ? 0 : -150
self.imageView.transform = self.imageView.transform.translatedBy(x: newX, y: newY)
}

How can I set the old transformation after the animation has finished?

I have a scene with several triangles on it with different colors and so on which the users can create when they touch the view. The triangle is placed where the user touched the view and is added to a topView (which is linked to a context menu but this is something different).
I want to apply the current transformation of my triangle after the animation has finished.
Right now it animates it 4 times and when the animation has finished it shrinks abrupt without an animation and places it not at the old position.
let invertedTransform = topView.transform.inverted()
let triangles = TriangleView()
triangles.transform = invertedTransform
topView.addsubview(triangles)
triangleArray.append(triangles)
for triangle in triangleArray {
UIView.animate(withDuration: 1, delay: 0, options: [.repeat,.autoreverse], animations: {
UIView.setAnimationRepeatCount(4)
triangle.transform = CGAffineTransform(scaleX: 2.4, y: 2.4) }, completion: { done in
if done {
triangle.transform = CGAffineTransform(scaleX: 1, y: 1)
}
})
}
I think the mistake is in the completion part.The .autoreverse option is doing the reverse animation fine but when it finishes it doesn't shrink back.
How can I save my the current transformation of my triangle and set a pulsating animation for it?
UPDATE
I forgot to mention that the every triangle has a rotation value which is stored in a database:
triangle.rotation.value
The default transformation which is indeed given through the .identity method is the rotation value 0. When a rotation is applied on a triangle object it should set it back to the original value which is stored in the triangle object above:
let triangles = TriangleView(x: touchpoint.x, y: touchpoint.y, rotation: triangle.rotation.value)
The completion part should probably be (not enough code provided to test it):
if done {
triangle.transform = CGAffineTransform.identity // or just .identity
}
That sets the transform state back to "original" / "no transform".
Edit
If your object already has a transform, you'll need to either save it or re-create it.
So, for example, add a property to your TriangleView that saves its "initial" state:
var initialTransform: CGAffineTransform!
When you init the view and setup the rotation transform, save that:
self.initialTransform = self.transform
Your animation completion then becomes:
if done {
triangle.transform = triangle.initialTransform
}

Keep zoom centered on UIView center (as opposed to scaling from top left corner)

I am implementing a pinch-based zoom and the scaling occurs from the top left corner of the view as opposed to scaling from the center. After a few attempts (this seems like a cs origin problem or the like), not finding a good solution for this but there must be some (perhaps obvious) way to scale from the view's center. If this has been answered before, would appreciate a pointer to the answer (not found after extensive search). If not, will appreciate inputs on correct approach.
Edit following answers (thanks):
Here is the code I was initially using:
func pinchDetected(pinchGestureRecognizer: UIPinchGestureRecognizer) {
let scale = pinchGestureRecognizer.scale
view.transform = CGAffineTransformScale(view.transform, scale, scale)
pinchGestureRecognizer.scale = 1.0
}
Upon pinch, the content inside the view would be expanding rightward and downward as opposed to same + leftward and upward (hence the assumption it is not scaling "from the center"). Hope this makes it clearer.
It's hard to know whats going on without seeing your code. By default transforms do act on a views centre, which seems to be what you want. You can make the transforms act on some other point by changing the anchorPoint property on the views layer.
Or you can create a transform about an arbitrary point by translating the origin to that point, doing your transform, and translating back again. e.g:
func *(left: CGAffineTransform, right: CGAffineTransform) -> CGAffineTransform {
return left.concatenating(right)
}
public extension CGAffineTransform {
static func scale(_ scale:CGFloat, aboutPoint point:CGPoint) -> CGAffineTransform {
let Tminus = CGAffineTransform(translationX: -point.x, y: -point.y)
let S = CGAffineTransform(scaleX: scale, y: scale)
let Tplus = CGAffineTransform(translationX: point.x, y: point.y)
return Tminus * S * Tplus
}
}
view.transform = CGAffineTransform.scale(2.0, aboutPoint:point)
where the point is relative to the origin, which by default is the center.
This is the code you are looking for
view.transform = CGAffineTransformScale(view.transform, 1.1, 1.1);
or in Swift
view.transform = view.transform.scaledBy(x: 1.1, y: 1.1)
This will increase views height and width by the provided scale.
Now you can control the amount by using a gesture recognizer.
You should be able to just use the code in this question to get it to zoom from the center. If you want it to zoom from the fingers, see the answer to that question.

dismiss view controller custom animation

I am trying to replicate this animation to dismiss a view controller (15 second video): https://www.youtube.com/watch?v=u87thAbT0CQ
This is what my animation looks like so far: https://www.youtube.com/watch?v=A2XmXTVxLdw
This is my code for the pan gesture recognizer:
#IBAction func recognizerDragged(sender: AnyObject) {
let displacement = recognizer.translationInView(view)
view.center = CGPointMake(view.center.x + displacement.x, view.center.y + displacement.y)
recognizer.setTranslation(CGPoint.zero, inView: view)
switch recognizer.state {
case .Ended:
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layer.position = CGPoint(x: self.view.frame.width / 2, y: self.view.frame.height / 2)
})
default:
print("default")
}
let velocity = recognizer.velocityInView(self.titleView)
print(velocity)
if velocity.y < -1500 {
up = true
self.dismissViewControllerAnimated(true, completion: nil)
}
if velocity.x > 1500 {
right = true
self.dismissViewControllerAnimated(true, completion: nil)
}
}
It may be a little hard to notice in my video, but there is a small disconnect in how fast the user flicks up, and how fast the animation completes. That is to say, the user may flip up very fast but the animation is set to a hardcoded 0.3 seconds. So if the user flicks the view fast, then as the animation completes, as soon as their finger lifts off the view, the animation actually slows down.
I think what I need is a way to take the velocity recorded in the recognizerDragged IBAction, and pass that to the animation controller, and based on that, calculate how long the animation should take, so that the velocity is consistent throughout, and it looks smooth. How can I do that?
Additioanlly, I'm slightly confused because the Apple Documentation says that the velocityInView function returns a velocity in points, not pixels. Yet different iOS devices have different points per pixels, so that would further complicate how I would translate the velocity before passing it to the animation class.
Any idea how to pass the velocity back to the animation controller, so that the animation duration changes based on that, and make it work for different iPhones ?
thanks
What you are likely looking at in the video you are trying to replicate is a UIDynamics style interaction, not a CoreAnimation animation. The velocity returned from velocityInView can be used directly in UIDynamics like this:
[self.behavior addLinearVelocity:
[pan velocityInView:pan.view.superview] forItem:pan.view];
I wrote a tutorial for doing this style of view interaction here: https://www.smashingmagazine.com/2014/03/ios7-new-dynamic-app-interactions/
To stick with UIView animations you just need to look at the frame's bottom (which is also in points) and calculate the new time. This assume that you want frame's bottom to be at 0 at the end of the animation:
animationTime = CGRectGetMaxY(frame) / velocity
You aren't showing how you created the animation controller, but just keep a reference to it and pass the time before calling dismiss. This is also assuming you are using a linear curve. With any other kind of curve, you will have to estimate what the starting velocity would have to be to be based on time and adjust.

How do I reset a UIView's position after a CATransform3D?

I have a UIView whose perspective I'd like to slightly change as the user rotates his device. For example when the user tilts to the right, I want the left side of the view to 'rise' while the right size 'goes in' but only to a certain point.
Here is my code so far:
var dynamicTransform = CATransform3DIdentity
dynamicTransform.m34 = 1/(-500)
// Device Tilt
if motionManager.gyroAvailable {
motionManager.gyroUpdateInterval = 0.1/30
motionManager.startGyroUpdates()
motionManager.startGyroUpdatesToQueue(NSOperationQueue.mainQueue(), withHandler: { (gyroData, NSError) -> Void in
// Perspective Transform
dynamicTransform = CATransform3DRotate(dynamicTransform, CGFloat(Double(gyroData!.rotationRate.y) * M_PI / 1080.0), 0, 0.1, 0)
dynamicTransform = CATransform3DRotate(dynamicTransform, -CGFloat(Double(gyroData!.rotationRate.x) * M_PI / 1080.0), 0.1, 0, 0)
self.blueView.layer.transform = dynamicTransform
})
Everything works great except for a few caveats:
1) When I return to the device's original orientation as when the device was launched, the view is distorted (i.e.there's still a perspective applied to it). There's probably something wrong with my logic but I don't see what..
2) Sometimes the view rotates clockwise/counterclockwise and doesn't remain still.
Any help would be greatly appreciated,
thanks!
Have you tried to send layoutIfNeeded to the parent view to force a re-layout after your orientation change?

Resources