4

We have a broken transition in WeatherKit only reproducible on iOS 13 beta. We're unsure if this is an UIKit bug or we're doing something awfully wrong.

With an array of UIViewPropertyAnimator working before iOS 13, ever since iOS 13 (through all of the betas) the animation frame is not updating correctly. For example, I have an UIViewPropertyAnimator called labelAnimator which animates a label to some specific CGRect, that CGRect is not respected and the label animates somewhere else as shown in the video.

Curious enough, if I mess around with the order of the transitions in the array, the bottom sheet works fine and the only one that animates wrong is the temperature label.

Here's the code that animates that whole view:

class MainView: UIViewController {
    var panGesture = UIPanGestureRecognizer()
    var tapGesture = UITapGestureRecognizer()
    let animationDuration: TimeInterval = 0.75
    var diff: CGFloat = 150

    @IBOutlet weak var gradientView: GradientView!
    @IBOutlet weak var detailedViewContainer: UIView!
    @IBOutlet weak var blurView: UIVisualEffectView!

    override func viewDidLoad() {
        self.panGesture.addTarget(self, action: #selector(MainView.handlePanGesture(gesture:)))
        self.detailedViewContainer.addGestureRecognizer(self.panGesture)
        self.tapGesture.addTarget(self, action: #selector(MainView.handleTapGesture(gesture:)))
        self.detailedViewContainer.addGestureRecognizer(self.tapGesture)
    }

    enum PanelState {
        case expanded
        case collapsed
    }

    var nextState: PanelState {
        return panelIsVisible ? .collapsed : .expanded
    }
    var panelIsVisible: Bool = false
    var runningAnimations = [UIViewPropertyAnimator]()
    var animationProgressWhenInterrupted: CGFloat = 0.0

     @objc func handleTapGesture(gesture: UITapGestureRecognizer)  {
        switch gesture.state {
        case .ended:
            tapAnimation()
        default: break
        }
    }

    @objc func tapAnimation(){
        self.panGesture.isEnabled = false
        self.tapGesture.isEnabled = false
        startInteractiveTransition(state: nextState, duration: animationDuration)
        updateInteractiveTransition(fractionComplete: 0)
        let linearTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.8, y: -0.16), controlPoint2: CGPoint(x: 0.22, y: 1.18))
        continueInteractiveTransition(timingParameters: linearTiming){
            self.panGesture.isEnabled = true
            self.tapGesture.isEnabled = true
        }
    }

     @objc func handlePanGesture(gesture: UIPanGestureRecognizer)  {
        switch gesture.state {
        case .began:
            if !panelIsVisible ? gesture.velocity(in: nil).y < 0 : gesture.velocity(in: nil).y > 0 {
                startInteractiveTransition(state: nextState, duration: animationDuration)
            }
        case .changed:
            let translation = gesture.translation(in: self.detailedViewContainer)
            var fractionComplete = (translation.y / view.bounds.height * 2)
            fractionComplete = !panelIsVisible ? -fractionComplete : fractionComplete
            updateInteractiveTransition(fractionComplete: fractionComplete)
        case .ended:
            let linearTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.8, y: -0.16), controlPoint2: CGPoint(x: 0.22, y: 1.18))

            continueInteractiveTransition(timingParameters: linearTiming) {
                self.panGesture.isEnabled = true
                self.tapGesture.isEnabled = true
            }
            NotificationCenter.default.post(name: .resetHeaders, object: nil)
            NotificationCenter.default.post(name: .disableScrolling, object: nil, userInfo: ["isDisabled": nextState == .collapsed])

        default:
            break
        }
    }

        // MARK: - Animations
    func animateTransitionIfNeeded(state: PanelState, duration: TimeInterval) {
        if runningAnimations.isEmpty {
            // MARK: Frame
            var linearTiming = UICubicTimingParameters(animationCurve: .easeOut)
            linearTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.1, y: 0.1), controlPoint2: CGPoint(x: 0.1, y: 0.1))
            let frameAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: linearTiming)
            frameAnimator.addAnimations {
                switch state {
                case .expanded:
                    self.detailedViewContainer.frame = CGRect(x: 0, y: self.diff, width: self.view.bounds.width, height: self.view.bounds.height - self.diff)
                case .collapsed:
                    self.detailedViewContainer.frame = CGRect(x: 0, y: self.view.bounds.height - self.view.safeAreaInsets.bottom - 165, width: self.view.bounds.width, height: 200)
                }
            }

            // MARK: Arrow
            let arrowAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: linearTiming)
            arrowAnimator.addAnimations {
                switch state {
                case .expanded:
                    self.leftArrowPath.transform = CGAffineTransform(rotationAngle: 15 * CGFloat.pi / 180)
                    self.rightArrowPath.transform = CGAffineTransform(rotationAngle: 15 * -CGFloat.pi / 180)
                case .collapsed:
                    self.leftArrowPath.transform = CGAffineTransform(rotationAngle: 15 * -CGFloat.pi / 180)
                    self.rightArrowPath.transform = CGAffineTransform(rotationAngle: 15 * CGFloat.pi / 180)
                }
                self.leftArrowPath.center.y = self.detailedViewContainer.frame.origin.y + 15
                self.rightArrowPath.center.y = self.detailedViewContainer.frame.origin.y + 15
            }

            // MARK: Scale
            let radiusAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: linearTiming)
            radiusAnimator.addAnimations{
                switch state {
                case .expanded:
                    self.gradientView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
                    self.gradientView.layer.maskedCorners = [.layerMaxXMinYCorner,.layerMinXMinYCorner]
                    self.gradientView.layer.cornerRadius = dataS.hasTopNotch ? 20 : 14
                case .collapsed:
                    self.gradientView.transform = CGAffineTransform.identity
                    self.gradientView.layer.maskedCorners = [.layerMaxXMinYCorner,.layerMinXMinYCorner]
                    self.gradientView.layer.cornerRadius = 0
                }
            }

            // MARK: Blur
            let blurTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.5, y: 0.25), controlPoint2: CGPoint(x: 0.5, y: 0.75))
            let blurAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: blurTiming)
            blurAnimator.addAnimations {
                switch state {
                case .expanded:
                    self.blurView.effect = UIBlurEffect(style: .dark)
                case .collapsed:
                    self.blurView.effect = nil
                }
            }

            // MARK: Text
            let textAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: linearTiming)
            textAnimator.addAnimations({
                switch state{
                case .expanded:
                    self.tempLabel.transform =  CGAffineTransform(scaleX: 0.6, y: 0.6)
                    self.tempLabel.frame =  CGRect(origin: CGPoint(x: 15, y: self.diff / 2 - self.tempLabel.frame.height / 2), size: self.tempLabel.frame.size)
                    self.descriptionLabel.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
                    self.descriptionLabel.alpha = 0
                    self.descriptionLabel.transform = CGAffineTransform(translationX: 0, y: -100)
                    self.summaryLabel.frame = CGRect(origin: CGPoint(x: self.blurView.contentView.center.x, y: 10), size: self.summaryLabel.frame.size)
                case .collapsed:
                    self.descriptionLabel.transform = CGAffineTransform.identity
                    self.descriptionLabel.alpha = 1
                    self.tempLabel.transform = CGAffineTransform.identity
                    self.tempLabel.frame = CGRect(origin: CGPoint(x: 15, y: self.view.frame.height / 2 - self.tempLabel.frame.height / 2 - 30), size: self.tempLabel.frame.size) 
                    self.summaryLabel.frame = CGRect(origin: CGPoint(x: self.blurView.contentView.center.x, y: self.tempLabel.center.y - self.summaryLabel.frame.height / 2), size: self.summaryLabel.frame.size)
                }
            }, delayFactor: 0.0)

            let summaryLabelTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.05, y: 0.95), controlPoint2: CGPoint(x: 0.15, y: 0.95))
            let summaryLabelTimingReverse = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.95, y: 0.5), controlPoint2: CGPoint(x: 0.85, y: 0.05))

            // MARK: Summary Label
            let summaryLabelAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: state == .collapsed ? summaryLabelTiming : summaryLabelTimingReverse)
            summaryLabelAnimator.addAnimations {
                switch state{
                case .expanded:
                    self.summaryLabel.alpha = 1
                case .collapsed:
                    self.summaryLabel.alpha = 0
                }
            }


            radiusAnimator.startAnimation()
            runningAnimations.append(radiusAnimator)

            blurAnimator.scrubsLinearly = false
            blurAnimator.startAnimation()
            runningAnimations.append(blurAnimator)


            summaryLabelAnimator.scrubsLinearly = false
            summaryLabelAnimator.startAnimation()
            runningAnimations.append(summaryLabelAnimator)

            frameAnimator.startAnimation()
            runningAnimations.append(frameAnimator)

            textAnimator.startAnimation()
            textAnimator.pauseAnimation()

            runningAnimations.append(textAnimator)

            arrowAnimator.startAnimation()
            runningAnimations.append(arrowAnimator)



            // Clear animations when completed
            runningAnimations.last?.addCompletion { _ in
                self.runningAnimations.removeAll()
                self.panelIsVisible = !self.panelIsVisible
                textAnimator.startAnimation()

            }
        }
    }

    /// Called on pan .began
    func startInteractiveTransition(state: PanelState, duration: TimeInterval) {
        if runningAnimations.isEmpty {
            animateTransitionIfNeeded(state: state, duration: duration)
            for animator in runningAnimations {
                animator.pauseAnimation()
                animationProgressWhenInterrupted = animator.fractionComplete
            }
        }

        let hapticSelection = SelectionFeedbackGenerator()
        hapticSelection.prepare()
        hapticSelection.selectionChanged()
    }

    /// Called on pan .changed
    func updateInteractiveTransition(fractionComplete: CGFloat) {
        for animator in runningAnimations {
            animator.fractionComplete = fractionComplete + animationProgressWhenInterrupted
        }
    }

    /// Called on pan .ended
    func continueInteractiveTransition(timingParameters: UICubicTimingParameters? = nil, durationFactor: CGFloat = 0, completion: @escaping ()->()) {
        for animator in runningAnimations {
            animator.continueAnimation(withTimingParameters: timingParameters, durationFactor: durationFactor)
        }
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + animationDuration) {
            completion()
        }

    }
}

And here's a video of the issue in iOS 13 and how it currently works in iOS 12.

perte
  • 261
  • 2
  • 3
  • 16

1 Answers1

0

I have same issue, for me UIViewPropertyAnimator continueAnimation durationFactor parameter is the issue, whenever it is not 0, after few animation the table view goes crazy.

Markicevic
  • 945
  • 7
  • 17