Why UIView.animate performs different result on view.layer and sublayer - ios

Here is my code:
let colorLayer = CALayer()
colorLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
colorLayer.backgroundColor = UIColor.blue.cgColor
let blockView = UIView.init(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
blockView.backgroundColor = UIColor.red
self.view.addSubview(blockView)
blockView.layer.addSublayer(colorLayer)
Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: { (t) in
UIView.animate(withDuration: 1.0) { [self] in
// this animation lasts 1s
// let transform = blockView.layer.affineTransform()
// blockView.layer.setAffineTransform(transform.rotated(by: CGFloat(Double.pi / 2.0)))
// while this animation 0.25s
let transform2 = colorLayer.affineTransform()
colorLayer.setAffineTransform(transform2.rotated(by: CGFloat(Double.pi / 2.0)))
} completion: { (complete) in
}
})
Why my custom sublayer colorLayer rotates by 0.25s (default value), not set by withDuration: parameter, but blockView.layer works fine?

UIView animations are meant to animate changes to animatable properties of UIView objects. They are not meant to animate CALayer properties.
Your code to change the colorLayer is probably not being animated by the UIView animation call at all. It is just that a CALayer's transform property supports implicit animations, so changing it triggers an implicit animation. I would expect that line to have exactly the same effect outside of a UIView animation.
I suspect that your call to apply an affine transform to your block view works because it ends changing the same transform property that you would change by using code like
blockView.transform = blockView.transform.rotated(by: CGFloat(Double.pi / 2.0)
The short answer: Don't try to animate layer properties using UIView animation. It usually doesn't work.

Related

CALayer Mask Animation not disappearing

Ive set up a splash screen like twitters opening, and it runs fine, but once the animation is complete, the mask returns to normal and doesn't disappear after widening. Here is the code for the mask & animation:
override func viewDidLoad() {
super.viewDidLoad()
//setting up the arm mask
imageView.image = UIImage(named: "placeholderbg.png")
self.mask = CALayer()
self.mask?.contents = UIImage(named: "arm.png")?.cgImage
self.mask?.bounds = CGRect(x: 0, y: 0, width: 120, height: 100)
self.mask?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.mask?.position = CGPoint(x: view.frame.size.width/2, y: view.frame.size.height/2)
imageView.layer.mask = mask
//animation of the mask
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
let keyFrameAnimation = CAKeyframeAnimation(keyPath: "bounds")
keyFrameAnimation.duration = 1
keyFrameAnimation.timingFunctions =
[CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut),
CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)]
let initialBounds = NSValue(cgRect: self.mask!.bounds)
let secondBounds = NSValue(cgRect: CGRect(x: 0, y: 0, width: 110, height: 90))
let finalBounds = NSValue(cgRect: CGRect(x: 0, y: 0, width: 6000, height: 5000))
keyFrameAnimation.values = [initialBounds, secondBounds, finalBounds]
keyFrameAnimation.keyTimes = [0, 0.3, 1]
self.mask!.add(keyFrameAnimation, forKey: "bounds")
}
}
image of what the mask returns to after animation is complete instead of disappearing
As with any animation, when the animation is over it is removed from the render tree and the "true" state of affairs is revealed. It is up to you to set the animated layer's true state to its final state, so that when the animation reaches its final frame and is removed, that final frame is matched by the layer's true state.
You are not doing that. You add the bounds animation to the mask layer, but you do not change the mask's real bounds. So when the animation ends, the mask layer appears to jump back to its initial bounds — because you never changed them.
As for the "disappear" part, it's hard to tell what you mean, but it's likely a different matter; I don't see anything in your code that would make the mask "disappear" so I'm not clear on what you expect. If you want something new to happen at the end of the animation, you need to attach a completion function to it.

How to animate scaling and moving a UILabel, then set its transform back to the identity when finished and preserve its frame?

I have a UILabel that I am attempting to scale and translate, and I want to animate this. I am doing this by setting its transform in a UIView.animate block. When the animation is finished, I would like to set the view's transform back to .identity and update its frame so that it remains exactly where the CGAffineTransform moved it. Pseudocode:
func animateMovement(label: UILabel,
newWidth: CGFloat,
newHeight: CGFloat,
newOriginX: CGFloat,
newOriginY: CGFloat)
{
UIView.animate(withDuration: duration, animations: {
label.transform = ??? // Something that moves it to the new location and new size
}) {
label.frame = ??? // The final frame from the above animation
label.transform = CGAffineTransform.identity
}
}
As to why I don't simply assign the new frame in the animation block: I have text inside the label that I want to scale with the animation, which is not possible when animating changing the frame instead of the transform.
I'm having coordinate space problems, which I can tell because after the animation it is not in the correct location (the label has the wrong origin).
Here is the answer I came up with. This will scale the contents through the duration of the animation and then reset the object to have the identity transform when finished.
static func changeFrame(label: UILabel,
toOriginX newOriginX: CGFloat,
toOriginY newOriginY: CGFloat,
toWidth newWidth: CGFloat,
toHeight newHeight: CGFloat,
duration: TimeInterval)
{
let oldFrame = label.frame
let newFrame = CGRect(x: newOriginX, y: newOriginY, width: newWidth, height: newHeight)
let translation = CGAffineTransform(translationX: newFrame.midX - oldFrame.midX,
y: newFrame.midY - oldFrame.midY)
let scaling = CGAffineTransform(scaleX: newFrame.width / oldFrame.width,
y: newFrame.height / oldFrame.height)
let transform = scaling.concatenating(translation)
UIView.animate(withDuration: duration, animations: {
label.transform = transform
}) { _ in
label.transform = .identity
label.frame = newFrame
}
}

UIView transform animations not being called

Currently there is a view I'm trying to translate(move), rotate and scale a view at the same time. For so strange reason it's only scaling it when I put scale at the bottom. But when I change the order and put the scaling first it rotates and translates the view properly but the scale of the view changes briefly to its correct scale before changing back to its original size. I need it to stay in it's scaled form. Here is the code:
class ViewController: UIViewController {
let newView = UIView(frame: CGRect(x: 10, y: 10, width: 100, height: 100))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(newView)
UIView.animate(withDuration: 1.0, animations: {
self.newView.transform = CGAffineTransform(translationX: 50, y: 70)//translation
self.newView.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)//rotation
self.newView.transform = CGAffineTransform(scaleX: 1, y: 0.5)//scale
})
}
}
Try combine the transforms first, then apply them at a time:
let translate = CGAffineTransform(translationX: 50, y: 70)
UIView.animate(withDuration: 1.0, animations: {
self.view.transform = translate.rotated(by: -CGFloat.pi / 2).scaledBy(x: 1, y: 0.5)
})

Centre animation on every screen size

I created this animation in the centre of the screen on a 6-inch size view. How can I make sure the ratio stays the same when it adapts to let's say an iPhone 5s. I'm not talking about Auto-Layout or constraints. I added the animation code below. This works the way I want it in a 6-inch size, but again, When I resize everything for iPhone 5s, Everything looks fine, besides the animation itself. How can I fix this?
[
iphone 6 animation screen above (This is the correct animation and how I want it to be position on the other screen size.)
override func viewDidLoad()
super.viewDidLoad() {
circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 300, height: 300)
self.view.addSubview(circleAnimationView)
}
The animation code is on on seperate viewcontroller, here it is:
import UIKit
class CirlceAnimationView: UIView {
var replicatorLayer1 = CAReplicatorLayer()
var dot = CALayer()
// Animation starts running
func animation2() {
// A layer that creates a specified number of copies of its sublayers (the source layer), each copy potentially having geometric, temporal, and color transformations applied to it.
replicatorLayer1 = CAReplicatorLayer()
// The layer’s bounds rectangle. Animatable.
replicatorLayer1.bounds = self.bounds
// The radius to use when drawing rounded corners for the layer’s background. Animatable.
replicatorLayer1.cornerRadius = 10.0
// The background color of the receiver. Animatable.
replicatorLayer1.backgroundColor = UIColor(white: 0.0, alpha: 0.0).cgColor
// The layer’s position in its superlayer’s coordinate space. Animatable.
replicatorLayer1.position = self.center
// calling this method creates an array for that property and adds the specified layer to it.
self.layer.addSublayer(replicatorLayer1)
// connectng the animation to the content
// An object that manages image-based content and allows you to perform animations on that content
dot = CALayer()
// The layer’s bounds rectangle. Animatable.
dot.bounds = CGRect(x: 0.0, y: 0.0, width: 12.0, height: 12.0)
//The layer’s position in its superlayer’s coordinate space. Animatable.
dot.position = CGPoint(x: 150.0, y: 40.0)
//The background color of the receiver. Animatable.
dot.backgroundColor = UIColor(white: 0.2, alpha: 1.0).cgColor
// The color of the layer’s border. Animatable.
dot.borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor
// The width of the layer’s border. Animatable.
dot.borderWidth = 1.0
//The radius to use when drawing rounded corners for the layer’s background. Animatable.
dot.cornerRadius = 5.0
//Appends the layer to the layer’s list of sublayers.
replicatorLayer1.addSublayer(dot)
// number of copies of layer is instanceCount
let nrDots: Int = 1000
//The number of copies to create, including the source layers.
replicatorLayer1.instanceCount = nrDots
// The basic type for floating-point scalar values in Core Graphics and related frameworks.
let angle = CGFloat(2*M_PI) / CGFloat(nrDots)
// The transform matrix applied to the previous instance to produce the current instance. Animatable.
replicatorLayer1.instanceTransform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0)
// Type used to represent elapsed time in seconds.
let duration: CFTimeInterval = 10.0
// animation capabilities for a layer property.
// An object that provides basic, single-keyframe animation capabilities for a layer property.
let shrink = CABasicAnimation(keyPath: "transform.scale")
// Defines the value the receiver uses to start interpolation.
shrink.fromValue = 1.0
// Defines the value the receiver uses to end interpolation.
shrink.toValue = 0.1
// Specifies the basic duration of the animation, in seconds.
shrink.duration = duration
// Determines the number of times the animation will repeat.
shrink.repeatCount = Float.infinity
// Add the specified animation object to the layer’s render tree.
dot.add(shrink, forKey: "shrink")
// Specifies the delay, in seconds, between replicated copies. Animatable.
replicatorLayer1.instanceDelay = duration/Double(nrDots)
// The transform applied to the layer’s contents. Animatable.
dot.transform = CATransform3DMakeScale(0.01, 0.01, 0.01)
}
}
Declare a variable for device width :
var DEVICE_WIDTH = ""
Then in ViewDidLoad :
let screenSize: CGRect = UIScreen.main.bounds
let screenWidth = screenSize.width
print(screenWidth)
// Detect the screen width (format purpose)
switch screenWidth {
case 320.0:
DEVICE_WIDTH = "320"
case 375.0:
DEVICE_WIDTH = "375"
case 414.0:
DEVICE_WIDTH = "414"
default: //320.0
DEVICE_WIDTH = "320"
}
Then in viewDidAppear :
switch DEVICE_WIDTH {
case "375": // 4/5
// according to your need
circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 250, height: 250)
case "414": //6
circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 300, height: 300)
default: //6+ (414)
// according to your need
circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 350, height: 350)
}
circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 300, height: 300)
Instead of hard coding those numbers, calculate them based on the superview bounds.
(But it would be better to position the circle animation view using auto layout.)

Playground code: AffineTransform and AnchorPoint don't work as I want

I want to shrink a triangle downward as below:
But the triangle shrink upward as below:
The transform code is below:
layer.setAffineTransform(CGAffineTransformMakeScale(1.0, 0.5))
I tried to use anchorPoint but I can't scceed.
//: Playground - noun: a place where people can play
import UIKit
let view = UIView(frame: CGRectMake(0, 0, 200, 150))
let layer = CAShapeLayer()
let triangle = UIBezierPath()
triangle.moveToPoint (CGPointMake( 50, 150))
triangle.addLineToPoint(CGPointMake(100, 50))
triangle.addLineToPoint(CGPointMake(150, 150))
triangle.closePath()
layer.path = triangle.CGPath
layer.strokeColor = UIColor.blueColor().CGColor
layer.lineWidth = 3.0
view.layer.addSublayer(layer)
view
layer.setAffineTransform(CGAffineTransformMakeScale(1.0, 0.5))
view
Anish gave me a advice but doesn't work well:
layer.setAffineTransform(CGAffineTransformMakeScale(2.0, 0.5))
Transforms operate around the layer's anchorPoint, which by default is at the center of the layer. Thus, you shrink by collapsing inwards, not by collapsing downwards.
If you want to shrink downward, you will need to scale down and change the view's position (or use a translate transform in addition to your scale transform), or change the layer's anchorPoint before performing the transform.
Another serious problem with your code is that your layer has no assigned size. This causes the results of your transform to be very misleading.
I ran this version of your code and got exactly the results you are asking for. I have put a star next to the key lines that I added. (Note that this is Swift 3; you really need to step up to the plate and update here.)
let view = UIView(frame:CGRect(x: 0, y: 0, width: 200, height: 150))
let layer = CAShapeLayer()
layer.frame = view.layer.bounds // *
let triangle = UIBezierPath()
triangle.move(to: CGPoint(x: 50, y: 150))
triangle.addLine(to: CGPoint(x: 100, y: 50))
triangle.addLine(to: CGPoint(x: 150, y: 150))
triangle.close()
layer.path = triangle.cgPath
layer.strokeColor = UIColor.blue.cgColor
layer.lineWidth = 3
view.layer.addSublayer(layer)
view
layer.anchorPoint = CGPoint(x: 0.5, y: 0) // *
layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 0.5))
view
Before:
After:

Resources