Почему UIView.animate работает с переходом интерактивного контроллера, а UIViewPropertyAnimator - нет?

Образец настройки распознавателя жестов и тому подобного для интерактивного перехода см. В этом ответе.

Я экспериментирую с интерактивными переходами и потратил немало времени, пытаясь выяснить, почему контроллеры будут переходить нормально, а не чистить в соответствии с жестом. Я обнаружил, что это не работает, потому что я использую UIViewPropertyAnimator, Переключение на более старые блоки анимации UIView работает из коробки. Зачем? Какая разница в реализации?

func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
{
    // Ignore the forced unwrapping, for sake of brevity.
    let view_From       = transitionContext.viewController(forKey: .from)!.view!
    let view_To         = transitionContext.viewController(forKey: .to)!.view!
    transitionContext.containerView.insertSubview(view_To, aboveSubview: view_From)

    view_To.alpha = 0

    // This animation block works - it will follow the progress value of the interaction controller
    UIView.animate(withDuration: 1, animations: {
        view_From.alpha = 0.0
        view_To.alpha = 1.0
    }, completion: { finished in
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
    })

    // This animation block fails - it will play out normally and not be interactive
    /*
    let animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
    animator.addAnimations {
        view_To.alpha = 1
        view_From.alpha = 0
    }
    animator.addCompletion { (position) in
        switch position {
        case .end: print("Completion handler called at end of animation")
        case .current: print("Completion handler called mid-way through animation")
        case .start: print("Completion handler called  at start of animation")
        }
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
    }
    animator.startAnimation()
    */
}

1 ответ

Решение

С введением UIViewPropertyAnimator в iOS 10 UIViewControllerAnimatedTransitioning Протокол тоже обновился. Они добавили необязательный func interruptibleAnimator(using: UIViewControllerContextTransitioning) что вам не нужно реализовывать (думаю, для обратной совместимости). Но он был добавлен именно для того случая использования, который вы упомянули здесь: чтобы воспользоваться преимуществами нового UIViewPropertyAnimator,

Итак, чтобы получить то, что вы хотите: во-первых, вы должны реализовать interruptibleAnimator(using:) создать аниматора - вы не создадите его в animateTransition(using:),

Согласно комментарию в исходном коде UIViewControllerAnimatedTransitioning (акцент мой)(я понятия не имею, почему документация не содержит эту информацию):

Соответствующий объект реализует этот метод, если создаваемый им переход может быть прерван. Например, он мог бы вернуть экземпляр UIViewPropertyAnimator. Ожидается, что этот метод будет возвращать один и тот же экземпляр в течение жизни перехода.

Вы должны вернуть тот же аниматор на время перехода. Вот почему вы найдете

private var animatorForCurrentSession: UIViewImplicitlyAnimating?

собственность в моем BackAnimator реализация - я сохраняю текущий аниматор, чтобы вернуть его, если переход еще не закончился.

Когда interruptibleAnimator(using:) реализуется, среда возьмет этот аниматор и использует его вместо анимации, используя animateTransition(using:) , Но чтобы сохранить договор протокола, animateTransition(using:) должен быть в состоянии оживить переход - но вы можете просто использовать interruptibleAnimator(using:) создать аниматор и запустить анимацию там.

Следующий рабочий BackAnimator реализация, которую вы можете использовать с примером, который вы упомянули в этом вопросе SO. Я использовал ваш код в качестве основы, но вы можете просто поменять мой BackAnimator для их реализации, и вы готовы пойти (я проверял это на их примере).

class BackAnimator : NSObject, UIViewControllerAnimatedTransitioning {
    // property for keeping the animator for current ongoing transition
    private var animatorForCurrentTransition: UIViewImplicitlyAnimating?

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }

    func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
        // as per documentation, the same object should be returned for the ongoing transition
        if let animatorForCurrentSession = animatorForCurrentTransition {
            return animatorForCurrentSession
        }
        // normal creation of the propertyAnimator
        let view_From       = transitionContext.viewController(forKey: .from)!.view!
        let view_To         = transitionContext.viewController(forKey: .to)!.view!
        transitionContext.containerView.insertSubview(view_To, aboveSubview: view_From)

        view_To.alpha = 0
        let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: .linear)
        animator.addAnimations {
            view_To.alpha = 1
            view_From.alpha = 0
        }
        animator.addCompletion { (position) in
            switch position {
            case .end: print("Completion handler called at end of animation")
            case .current: print("Completion handler called mid-way through animation")
            case .start: print("Completion handler called  at start of animation")
            }
            // transition completed, reset the current animator:
            self.animatorForCurrentTransition = nil

            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
        // keep the reference to current animator
        self.animatorForCurrentTransition = animator
        return animator
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // animateTransition should work too, so let's just use the interruptibleAnimator implementation to achieve it
        let anim = self.interruptibleAnimator(using: transitionContext)
        anim.startAnimation()
    }
}

Также обратите внимание, что аниматор вернулся interruptibleAnimator(using:) не запускается нами - среда запускает его, когда это необходимо.

PS: Большая часть моих знаний по этому вопросу происходит от попыток реализовать контейнер с открытым исходным кодом, который бы позволял настраивать интерактивные переходы между его контрагентами - https://github.com/MilanNosal/InteractiveTransitioningContainer. Может быть, вы тоже найдете там вдохновение:).

Другие вопросы по тегам