Animate custom UIViews like layer layer during UIView.animate?

Imagine a tall, thin look, we will say that it looks like a ladder.

It is 100 high. We animate it so that it is the full height of the screen. So, in the pseudo code ..

func expandLadder() { view.layoutIfNeeded() UIView.animate(withDuration: 4 ... ladderTopToTopOfScreenConstraint = 0 ladderBottomToBottomOfScreenConstraint = 0 view.layoutIfNeeded() } 

No problems.

Anyway. We draw a ladder in the Subviews layout. (Our staircase is just a thick black line.)

 @IBDesignable class LadderView: UIView { override func layoutSubviews() { super.layoutSubviews() setup() } func setup() { heightNow = frame size height print("I've recalculated the height!") shapeLayer ... topPoint = (0, 0) bottomPoint = (0, heightNow) p.addLines(between: [topPoint, bottomPoint]) shapeLayer!.path = p shapeLayer add a CABasicAnimation or whatever layer.addSublayer(shapeLayer!) } 

No problems.

Thus, we simply create a line p that fills the height of the "heightNow" of the view.

The problem, of course, when you do this ...

 func expandLadder() { view.layoutIfNeeded() UIView.animate(withDuration: 4 ... ladderToTopOfScreenConstraint = 0 ladderBottomOfScreenConstraint = 0 view.layoutIfNeeded() } 

... when you do this, LayoutSubviews (or setBounds, or draw # rect, or whatever you can think of) is called only before and after the UIView.animate animation. This is a real pain.

In this example, the "actual" shapeLayer ladder will go to the following values ​​before you animate the size of the LIE view: "I recalculated the height!" called only once before and once after animating UIView.animate. Assuming you don’t crop the subviews, you will see a “wrong, upcoming” size before each animation of length L.)

What's the solution?


By the way, of course, you can do this by using draw # rect and animating yourself, i.e. with CADisplayLink (as mentioned by Josh). So good. It is incredibly simple to draw shapes, simply using a layer / path: my question here is how to make, in the example, the code in setup () run at each stage of the animation of the UIView constraints of the type in question.

+7
xcode animation swift core-animation
source share
2 answers

The behavior you describe is related to how iOS animations work. In particular, when you set the presentation frame in the UIAnimation block (what layoutIfNeeded does), it leads to a synchronized recalculation of the frame in accordance with the auto-detection rules), the presentation frame immediately changes to a new value, which is the visible value at the end of the animation (add the instruction print after layoutIfNeeded and you will see that it is true). By the way, changing the constraints does nothing until layoutIfNeeded is computed, so you don’t even need to put the constraint change in the animation block.

However, during the animation, the system displays the presentation level instead of the support layer and interpolates the frame value from the presentation level from the initial value to the final value during the duration of the animation. This makes it look like the view is moving, although checking the value will show that the view has already reached its final position.

Therefore, if you want the actual coordinates of the layer on the screen to be in flight during the animation, you must use view.layer.presentationLayer to get them. See: https://developer.apple.com/reference/quartzcore/calayer/1410744-presentationlayer

This does not completely solve your problem due to the fact that I assume that what you are trying to do is to increase the number of layers from time to time. If you can't do it with simple interpolation, then you might be better off spawning all the layers at the beginning, and then using keyframe animations to turn them on at regular intervals as the animation progresses. You can base your layer positions on view.layer.presentationLayer.bounds (your sublayers are in super-slot coordinates, so don't use a frame). It is difficult to provide an accurate solution without knowing what you are trying to accomplish.

Another possible solution is to use drawRect with CADisplayLink, which allows you to redraw each frame as you want, so it is much more suitable for things that cannot be easily interpolated from frame to frame (for example, games or in your case add / remove and animation geometry as a function of time).

Edit: Here is an example of a playground showing how to animate a layer in parallel with a view.

  //: Playground - noun: a place where people can play import UIKit import PlaygroundSupport class V: UIViewController { var a = UIView() var b = CAShapeLayer() var constraints: [NSLayoutConstraint] = [] override func viewDidLoad() { view.addSubview(a) a.translatesAutoresizingMaskIntoConstraints = false constraints = [ a.topAnchor.constraint(equalTo: view.topAnchor, constant: 100), a.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100), a.widthAnchor.constraint(equalToConstant: 200), a.heightAnchor.constraint(equalToConstant: 200) ] constraints.forEach {$0.isActive = true} a.backgroundColor = .blue } override func viewDidAppear(_ animated: Bool) { b.path = UIBezierPath(ovalIn: a.bounds).cgPath b.fillColor = UIColor.red.cgColor a.layer.addSublayer(b) constraints[3].constant = 400 constraints[2].constant = 400 let oldBounds = a.bounds UIView.animate(withDuration: 3, delay: 0, options: .curveLinear, animations: { self.view.layoutIfNeeded() }, completion: nil) let scale = a.bounds.size.width / oldBounds.size.width let animation = CABasicAnimation(keyPath: "transform.scale") animation.fromValue = 1 animation.toValue = scale animation.duration = 3 b.add(animation, forKey: "scale") b.transform = CATransform3DMakeScale(scale, scale, 1) } } let v = V() PlaygroundPage.current.liveView = v 
+4
source share

layoutIfNeeded() will only call layoutSubviews() when necessary. I do not see the code setNeedsLayout() in the code. The way this is done when there is no limit! If you have limitations, you should call setNeedsUpdateConstraints() in your animation block layoutIfNeeded .

 view.setNeedsUpdateConstraints() UIView.animate(...) { ... view.layoutIfNeeded() } 
0
source share

All Articles