I'm trying to add this 3D animation but it's not animating it properly. There is a jerk during the animation.
var currentTransform = CATransform3DIdentity
UIView.animate(withDuration: 0.3, animations: {
currentTransform = CATransform3DMakeScale(0.5, 0.5, 1)
currentTransform.m34 = 1.0 / -800
currentTransform = CATransform3DRotate(currentTransform, CGFloat(M_PI) * 50.0/180.0, 0, 1, 0)
self.containerView.layer.transform = currentTransform
})
What can be the problem and how to solve it?
UPDATE (Answer):
This piece of code works fine. The only problem was, the containerView was an immediate subView of my main view and this was causing the jerk.
The solution to this is, I added another view to my main view and made the containerView a subview of the newly added view.
Though I'm not sure why this happened. I think, the transformation I was doing on my containerView, also affected the layer of the main view through transformation.
Related
In my app I have several buttons that, after a separate button is pressed move to new positions in a UIView animation. The animation itself works perfectly, but the problem is, after the animation, the four buttons stop responding to touch actions. Here's my current code:
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.allowUserInteraction], animations: {
self.hexagon.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2)
self.tileB.center.x += self.tileB.bounds.width
self.tileB.center.y += self.tileB.bounds.height
self.tileF.center.x -= self.tileF.bounds.width
self.tileF.center.y += self.tileF.bounds.height
self.tileH.center.x -= self.tileH.bounds.width
self.tileH.center.y -= self.tileH.bounds.height
self.tileD.center.x += self.tileD.bounds.width
self.tileD.center.y -= self.tileD.bounds.height
}, completion: nil)
I have tried many other methods, including setting frames and bounds, but none of them have worked correctly.
The animation itself works perfectly, but the problem is, after the animation, the four buttons stop responding to touch actions
The reason is probably that after the animation, the buttons have moved out of their superview's bounds. A view outside of its superview is untouchable, even if it is visible.
One way to confirm this is to set the superview's clipsToBounds to true and do the animation. The buttons will vanish by the end of the animation, proving that they have moved outside their superview.
I am using SpreadsheetView and trying to zoom into it using a pinch gesture and a scale transform. The view zooms fine but when I try to reload the data after it has been zoomed out, the frame of the view is much smaller than it should be. This is the snippet of code that handles the zooming of the view -
#IBAction func handlePinch(recognizer : UIPinchGestureRecognizer) {
if recognizer.state == .began {
lastScale = Float(recognizer.scale)
}
if recognizer.state == .began || recognizer.state == .changed {
currentScale = recognizer.view?.layer.value(forKeyPath: "transform.scale") as! Float
var newScale = 1 - (lastScale - Float(recognizer.scale))
newScale = min(newScale, SpreadsheetViewController.kMaxScale / currentScale)
newScale = max(newScale, SpreadsheetViewController.kMinScale / currentScale)
spreadsheetView.transform = spreadsheetView.transform.scaledBy(x: CGFloat(newScale), y: CGFloat(newScale))
lastScale = Float(recognizer.scale)
}
if recognizer.state == .ended {
spreadsheetView.frame = CGRect(x: 0, y: topView.frame.maxY, width: view.frame.size.width, height: view.frame.size.height - topView.frame.maxY)
print(spreadsheetView.frame.size.width)
print(spreadsheetView.scrollView.frame.size.width)
}
}
I am explicitly setting the frame of the spreadsheetView to cover the frame of the UIViewController after the pinch gesture has ended and it manages to stick to that frame after reloading but I think its the frame of the UIScrollView inside the SpreadsheetView that loses its frame.
The SpreadsheetView seems to use its frame size properties to draw its content. Modifying the transform will change the frame values accordingly, and so using these (instead of the bounds sizes) will make the end result wrong, in your case effectively scaling down twice. This is likely a miss in the SpreadsheetView.
You can work around it by using a container for the SpreadsheetView. Create a regular UIView to do the transforms on, and embed the SpreadsheetView as a child, without any transforms.
I did a quick test on one of the demo apps included with the SpreadsheetView, and it confirms my thoughts. Doing a simple transform directly on the SpreadsheetView with scale 0.5, 0.5, and set the background color to red, results in this picture. Note that the red background is visible, which it shouldn't.
The same test using a UIView as a container for the SpreadsheetView, scaling the container to 0.5, 0.5, and setting the container's background to red, results in this picture. Works as intended.
Is there any way to animate a height constraint using an easing curve? I'm thinking of maybe some way to set up a CAKeyFrameAnimation with the values and timings I'd like, and somehow have it affect the height constraint.
I'm not sure if there's a separate animation calss for constraints that I can utilize for constraint keyFrames, or if there's something I'm missing with CAKeyFrameAnimation that would allow me to use that class, or if it's not possible.
Edit: Here's what I'm trying to make work for me, if anyone knows if I'm on the right path or not I'd appreciate some guidance:
self.heightConstraint.constant = newHeight
let animation = CAKeyframeAnimation(keyPath: "frame.size.height")
animation.values = [self.frame.size.height, newHeight]
animation.keyTimes = [0, 1]
animation.duration = self.animationDuration
animation.delegate = self
self.layer.add(animation, forKey: "heightChange")
newHeight would be whatever I want the height to be. But this just pops the view to be taller without any animation. Am I using this correctly? Is this possible to do with constraints?
Edit 2: I should add, I want to use more complex easing functions than the defaults Apple provides as part of UIView.animate(withDuraiton:....
There is no need to use CAKeyframeAnimation. A UIView animation will do easing on animations to a constraint, just like any other animation:
myConstraintOutlet.constant = someNewValue
UIView.animate(
duration: 0.5,
delay: 0.0,
options: .curveEaseInOut, //Use ease-in, ease-out timing.
animations: {
self.view.layoutIfNeeded()
},
completion: nil)
Edit:
If you want custom timing, you could also use the UIView-based keyframe animation with the method animateKeyframes(withDuration:delay:options:animations:completion:) (UIView animation is a whole lot easier to use than CAAnimations.)
Edit #2:
Or, if all you need is a different cubic easing curve, you can do that too. See the very last bit at this link: https://medium.com/#RobertGummesson/a-look-at-uiview-animation-curves-part-3-edde651b6a7a
The key bit is this code snippet from that link:
circleView.transform = CGAffineTransformMakeScale(0, 0)
let timingFunction = CAMediaTimingFunction(controlPoints: 5/6, 0.2, 2/6, 0.9)
CATransaction.begin()
CATransaction.setAnimationTimingFunction(timingFunction)
UIView.animateWithDuration(1) {
self.circleView.transform = CGAffineTransformIdentity
}
CATransaction.commit()
(That's not my code, it's Robert Gummesson's, from the link above. All credit for writing it goes to him.)
In one of my ViewControllers, I have addStatus(), which is pasted below, in my viewDidLoad. I expect the lineWidth of this circle to animate, but it doesn't seem to be doing so. In my search for why this might be, I found this part of Apple's documentation on Animating Layer Content. The part that I thought to be important noted here:
If you want to use Core Animation classes to initiate animations, you must issue all of your Core Animation calls from inside a view-based animation block. The UIView class disables layer animations by default but reenables them inside animation blocks. So any changes you make outside of an animation block are not animated. Listing 3-5 shows an example of how to change a layer’s opacity implicitly and its position explicitly. In this example, the myNewPosition variable is calculated beforehand and captured by the block. Both animations start at the same time but the opacity animation runs with the default timing while the position animation runs with the timing specified in its animation object.
When I looked up why this might not be animating, I read this piece and assumed that it meant I should place my CAAnimation inside of a UIView animation block. This function worked fine in a blank application without the animation block when placed in the rootViewController, but does not seem to animate when in my secondary viewController in this app. Any tips would be wonderfully helpful. Thanks!
func addStatus() {
UIView.animateWithDuration( NSTimeInterval.infinity, animations: { () -> Void in
let patientZeroIndicator = CAShapeLayer()
let radius:CGFloat = 20.0
let center:CGPoint = CGPointMake(self.view.frame.width - radius - 10, radius + 10)
let startAngle = 0.0
let endAngle = 2.0 * Double(M_PI)
patientZeroIndicator.lineWidth = 10.0
patientZeroIndicator.fillColor = UIColor(netHex: cs_red).CGColor
patientZeroIndicator.strokeColor = UIColor.whiteColor().CGColor
patientZeroIndicator.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true).CGPath
self.view.layer.addSublayer(patientZeroIndicator)
// Create a blank animation using the keyPath "cornerRadius", the property we want to animate
let pZeroAnimation = CABasicAnimation(keyPath: "lineWidth")
// Define the parameters for the tween
pZeroAnimation.fromValue = 10.0
pZeroAnimation.toValue = 5.0
pZeroAnimation.autoreverses = true
pZeroAnimation.duration = 3.0
pZeroAnimation.repeatDuration = CFTimeInterval.infinity
pZeroAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0, 0.75, 1)
// Finally, add the animation to the layer
patientZeroIndicator.addAnimation(pZeroAnimation, forKey: "lineWidth")
})
}
in my viewDidLoad.
That is your problem. The viewDidLoad method runs before the view has been added to a window. You can't add animations to a view unless it's in a window. Call addStatus in viewDidAppear: instead.
Also, don't create new layers in your animation block. Create the layer in viewDidLoad.
I'm trying to create a "page flip effect" using UIView instead of CALayer due to a project limitation. This requires flipping 1 UIView 180 degrees and essentially "sticking it" to the back of the other UIView. You then rotate the two UIViews simultaneously by rotating the superview in 3D space.
I'm trying to port AFKPageFlipper's "initFlip" method to use UIView instead of UIImage.
Below is a snippet of my attempt to port it. The initial page flip works, but the "front layer" in the code doesn't seem to show up. As if I"m not able to see the backend of the page. When I'm flipping the page, the animation is initially correct (back layer is fine), but then the other side of the page (front layer), I see the inverted view of the first page (backLayer).
Any help would be awesome!
flipAnimationLayer = [[UIView alloc] init];
flipAnimationLayer.layer.anchorPoint = CGPointMake(1.0, 0.5);
flipAnimationLayer.layer.frame = rect;
[self addSubview:flipAnimationLayer];
UIView *backLayer;
UIView *frontLayer;
if (flipDirection == AFKPageFlipperDirectionRight)
{
backLayer = currentViewSnap2;
backLayer.layer.contentsGravity = kCAGravityLeft;
frontLayer = nextViewSnap2;
frontLayer.layer.contentsGravity = kCAGravityRight;
}else
{
backLayer = nextViewSnap2;
backLayer.layer.contentsGravity = kCAGravityLeft;
frontLayer= currentViewSnap2;
frontLayer.layer.contentsGravity = kCAGravityRight;
}
backLayer.frame = flipAnimationLayer.bounds;
backLayer.layer.doubleSided = NO;
backLayer.clipsToBounds = YES;
[flipAnimationLayer addSubview:backLayer];
frontLayer.frame = flipAnimationLayer.bounds;
frontLayer.layer.doubleSided = NO;
frontLayer.clipsToBounds = YES;
frontLayer.layer.transform = CATransform3DMakeRotation(M_PI, 0, 1.0, 0);
[flipAnimationLayer addSubview:frontLayer];
if (flipDirection == AFKPageFlipperDirectionRight)
{
CATransform3D transform = CATransform3DMakeRotation(0.0, 0.0, 1.0, 0.0);
transform.m34 = 1.0f / 2500.0f;
flipAnimationLayer.layer.transform = transform;
currentAngle = startFlipAngle = 0;
endFlipAngle = -M_PI;
} else
{
CATransform3D transform = CATransform3DMakeRotation(-M_PI / 1.1, 0.0, 1.0, 0.0);
transform.m34 = 1.0f / 2500.0f;
flipAnimationLayer.layer.transform = transform;
currentAngle = startFlipAngle = -M_PI;
endFlipAngle = 0;
}
Your code is rotating layers, not views. That's fine.
I would not expect the code you posted to animate, since a layer's backing view doesn't do implicit animation, You could make it animate by using a CABasicAnimation. Or, you could create layers for your front and back views and attach them as sublayers of your view's layers. If you do that than manipulating the transform on the layers will use implicit animations.
What I've done to create my own font-to-back flip as you describe is to fake it.
I animate in 2 steps: First from zero degrees (flat) to 90 degrees (where the layers become invisible.) At that moment I hide the first layer and make the second layer visible, rotated 90 degrees the other way, and then rotate the other layer back to zero. This creates the same visual effect as showing the back face of the rotation.
If you use implicit layer animation to do this you'll need to put the changes to the transform inside a CATransaction block and set the animation timing to linear, or use ease-in for the first half and ease-out for the second half. That's because animations default to ease-in,ease-out timing, and the first animation to 90 degrees will slow down at the end, and then the second 90 degree animation will ease in.