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

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?

Related

Smooth Animation Troubles

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.

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.

CGAffineTransfrom: Translating after a rotation

I am trying to animate a rectangle after I have rotated it with CGAffineTransform. The issue I am having is that the rectangle ends up where it is suppose to be, but the "starting" position for the second transform is not what I expect. This is only an issue because I am animating it. Here is my code below:
UIView.animate(withDuration: 5) {
let shift = 200 * CGFloat(2.0.squareRoot() / 2)
view.transform = view.transform.translatedBy(x: shift, y: 0)
}
view is already defined elsewhere and I have already rotated the view with this:
let rotation = CGAffineTransform(rotationAngle: (45/180)*CGFloat(M_PI))
view.transform = rotation
Before the animation, this is what it looks like: 1
For some reason, the transformation starts above (a little offscreen) and then moves down into position. 2
I would like this to happen instead, where it starts from the original picture, and then shifts in the direction of how its rotated. 3
Note I did try to apply the same shift value to both x and y for the translation but that did not fix my issue.

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.

Core Motion: how to tell which way is "up"?

I'm trying to duplicate the functionality in the Compass app - and I'm stuck on a particular bit: how do I figure out which way is "up" in the interface?
I've got a label onscreen, and I've got the following code that orients it to remain horizontal as the device moves around:
self.motionManager = CMMotionManager()
self.motionManager?.gyroUpdateInterval = 1/100
self.motionManager?.startDeviceMotionUpdatesToQueue(NSOperationQueue.mainQueue(), withHandler: { (deviceMotion, error) -> Void in
let roll = -deviceMotion.attitude.roll
self.tiltLabel?.transform = CGAffineTransformRotate(CGAffineTransformIdentity, CGFloat(roll))
})
This effect is pretty good, but it's got a few states where it's wrong - for example, the label flips erratically when the iPhone's lightning connector is pointed up.
How do I consistently tell which direction is up using CoreMotion?
UPDATE: Apparently, roll/pitch/yaw are Euler angles, which suffer from gimbal lock - so I think the correct solution might involve using quaternions, which don't suffer from this issue, or perhaps the rotationMatrix on CMAttitude might help: https://developer.apple.com/library/ios/documentation/CoreMotion/Reference/CMAttitude_Class/index.html
It doesn't need to be quite so complicated for the 2D case. "Up" means "opposite gravity", so:
motionManager.startDeviceMotionUpdatesToQueue(NSOperationQueue.mainQueue()) { (motion, error) in
// Gravity as a counterclockwise angle from the horizontal.
let gravityAngle = atan2(Double(motion.gravity.y), Double(motion.gravity.x))
// Negate and subtract π/2, because we want -π/2 ↦ 0 (home button down) and 0 ↦ -π/2 (home button left).
self.tiltLabel.transform = CGAffineTransformMakeRotation(CGFloat(-gravityAngle - M_PI_2))
}
But simply "opposite gravity" has less meaning if you're trying to do this in all 3 dimensions: the direction of gravity doesn't tell you anything about the phone's angle around the gravity vector (if your phone is face-up, this is the yaw angle). To correct in three dimensions, we can use the roll, pitch, and yaw measurements instead:
// Add some perspective so the label looks (roughly) the same,
// no matter what angle the device is held at.
var t = self.view.layer.sublayerTransform
t.m34 = 1/300
self.view.layer.sublayerTransform = t
motionManager.startDeviceMotionUpdatesToQueue(NSOperationQueue.mainQueue()) { (motion, error) in
let a = motion.attitude
self.tiltLabel.layer.transform =
CATransform3DRotate(
CATransform3DRotate(
CATransform3DRotate(
CATransform3DMakeRotation(CGFloat(a.roll), 0, -1, 0),
CGFloat(a.pitch), 1, 0, 0),
CGFloat(a.yaw), 0, 0, 1),
CGFloat(-M_PI_2), 1, 0, 0) // Extra pitch to make the label point "up" away from gravity
}

Resources