0

I'm creating inside my app this customized view that can be expanded by the user with a drag and has all the animations of the case. Just to be clear, I want to reproduce pretty much the same animation of the control center of IOS. So far I managed to obtain pretty much everything from the animation of the view's expansion when the finger drags it, to the possibility of make the corners rounded.

Now, the fact is that, besides the possibility to drag the view, I want to implement an animation when the user takes off the finger in between the expansion in order to make the view come back to its original height or finish the expansion. To do that I'm using the position of the view when the user stops dragging.

The issues started when I tried to animate the UIView.layer.mask. With some research on the internet, I discovered the CABasicAnimation class and I implemented it inside a function:

func animateExpandableView(withDuration duration: Double) {

    CATransaction.begin()
    CATransaction.setAnimationDuration(duration)
    CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeInEaseOut))

    let maskAnimation = CABasicAnimation(keyPath: "mask")
    maskAnimation.fromValue = profileView.layer.mask

    let bounds = profileView.frame
    let maskPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 8.0, height: 8.0))
    let maskLayer = CAShapeLayer()
    maskLayer.frame = bounds
    maskLayer.path = maskPath.cgPath

    maskAnimation.toValue = maskLayer
    maskAnimation.duration = duration

    profileView.layer.mask = maskLayer
    profileView.layer.add(maskAnimation, forKey: "mask")

    CATransaction.commit()


}

Btw this doesn't work and the change of the mask isn't animated. Where did I make a mistake in the implementation?

Someone suggested to me to check this link from another question; I didn't find it very useful because, despite the fact it didn't work for my case, the answer is written in C and not in Swift.

Animating a CALayer's mask size change

Lorenzo Santini
  • 655
  • 7
  • 13
  • Why are you filtering the list of constraints? Why not simply store a reference to the constraint in a property when you create it? You will probably find this easier using a `UIViewPropertyAnimator` – Paulw11 Feb 21 '19 at 21:36
  • @Paulw11 I’m still a noob I’ll follow your suggestion :)...what’s a UiPropertyViewAnimator and why shouldn’t I use Uiview.animate? Thanks – Lorenzo Santini Feb 21 '19 at 21:39
  • https://developer.apple.com/documentation/uikit/uiviewpropertyanimator – Paulw11 Feb 21 '19 at 21:43
  • Ok thanks i’ll check – Lorenzo Santini Feb 21 '19 at 21:45
  • Just one more question...how can i create a reference to the constraint? – Lorenzo Santini Feb 21 '19 at 22:00
  • When you create it, save the reference in a property – Paulw11 Feb 21 '19 at 22:02
  • You’re right...thank you – Lorenzo Santini Feb 21 '19 at 22:05
  • @Paulw11 I tried to use a 'UIViewPropertyAnimator' but nothing changed...btw I updated the code above with the change. What's wrong with that? – Lorenzo Santini Feb 22 '19 at 15:16
  • Possible duplicate of [Animating a CALayer's mask size change](https://stackoverflow.com/questions/2910770/animating-a-calayers-mask-size-change) – EmilioPelaez Feb 24 '19 at 14:53
  • The mask property is not animatable, somebody already had a similar issue here https://stackoverflow.com/questions/2910770/animating-a-calayers-mask-size-change – EmilioPelaez Feb 24 '19 at 14:54
  • @EmilioPelaez I have looked into the question you linked but I don't know C so I didn't understand very much of it...In the link you send the say it's possible to animate it, why you now say it's not? – Lorenzo Santini Feb 24 '19 at 15:04
  • An `animatable` property is one that will be animated by the system, `mask` is not `animatable` so the change will happen immediately instead. The top answer of the question I linked has a decent workaround, thought it probably won't be the exact answer you need, you'll have to put some work into it. You could use the layers opacity. – EmilioPelaez Feb 24 '19 at 17:14
  • @ EmilioPelaez Ok thanks – Lorenzo Santini Feb 24 '19 at 17:28

1 Answers1

0

I found a workaround to the fact that the mask property can't be animated by using a timer to recreate the animation. Basically, I divide the animationDuration by 100 and set the result as the timer's interval. Then I do the same with the difference between the finalHeight and the initalHeight dividing it by 100 and assigning it to animationStep. With all of that set up I just run the timer for 100 cycles and during each one add animationStep to the maskHeight property. Here's all the code from my class:

class ExpandableView: UIView {
    //Timer to animate view's mask
    private var animationTimer: Timer? = nil

    //the height of each step for the mask's animation
    private var animationStep: CGFloat? = nil
    private var currentStep: Double = 0

    //Variable responsable to return and set self.mask height.
    private var maskCornerRadii: CGSize? = nil
    private var maskRoundingCorners: UIRectCorner? = nil

    //Variable responsable to return and set self.mask height. it's nil if there isn't any mask
    private var maskHeight: CGFloat? {
        get {return self.layer.mask?.bounds.height}
        set {
            if newValue != nil && maskRoundingCorners != nil && maskCornerRadii != nil {
                let frame = self.bounds
                let bounds = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: newValue!)
                let maskPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: maskRoundingCorners!, cornerRadii: maskCornerRadii!)
                let maskLayer = CAShapeLayer()
                maskLayer.frame = bounds
                maskLayer.path = maskPath.cgPath

                self.layer.mask = maskLayer
            }
        }
    }


    //Animation function
    func animateMask(from initialHeight: CGFloat, to finalHeight: CGFloat, withDuration duration: Double) {
        let timerInterval = duration * 0.01
        animationStep = (finalHeight - initialHeight) * 0.01
        animationTimer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true, block: { (timer) in

            if self.currentStep <= 100 {
                guard self.animationStep != nil else {
                    fatalError("animationStep is nil")
                }

                guard self.maskHeight != nil else {
                    fatalError("Mask is nil")
                }

                self.maskHeight! += self.animationStep!
                self.currentStep += 1

            } else {
                self.animationTimer!.invalidate()
                self.currentStep = 0
            }
        })
    }
}
Lorenzo Santini
  • 655
  • 7
  • 13