Я пытаюсь реализовать собственный жест pan
для интерактивного перехода на новый view controller
. Это работает так, что у меня есть кнопка (помеченная «Редактор шаблонов», см. Ниже), на которой вы можете запустить pan
, чтобы переместить текущий view controller
вправо, открывая новый view controller
рядом с ним ( Я записал свою проблему, см. Ниже).
Все работает, но есть ошибка, которую я совсем не понимаю:
Иногда, когда я просто провожу пальцем по кнопке (вызывая жест pan
), затем снова поднимаю палец (касание вниз -> быстрое короткое движение вправо -> касание) интерактивный переход глюки. Он начинает очень медленно завершать переход, и после этого я не могу отклонить представленный view controller
и не могу ничего представить на представленном view controller
.
Понятия не имею, почему. Вот мой код:
Во-первых, класс UIViewControllerAnimatedTransitioning
. Он реализован с использованием UIViewPropertyAnimator
и просто добавляет анимацию с использованием transform
:
class MovingTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
enum Direction {
case left, right
}
// MARK: - Properties
// ========== PROPERTIES ==========
private var animator: UIViewImplicitlyAnimating?
var duration = 0.6
var presenting = true
var shouldAnimateInteractively: Bool = false
public var direction: Direction = .left
private var movingMultiplicator: CGFloat {
return direction == .left ? -1 : 1
}
// ====================
// MARK: - Initializers
// ========== INITIALIZERS ==========
// ====================
// MARK: - Overrides
// ========== OVERRIDES ==========
// ====================
// MARK: - Functions
// ========== FUNCTIONS ==========
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let animator = interruptibleAnimator(using: transitionContext)
animator.startAnimation()
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
// If the animator already exists, return it (important, see documentation!)
if let animator = self.animator {
return animator
}
// Otherwise, create the animator
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
if presenting {
toView.frame = containerView.frame
toView.transform = CGAffineTransform(translationX: movingMultiplicator * toView.frame.width, y: 0)
} else {
toView.frame = containerView.frame
toView.transform = CGAffineTransform(translationX: -movingMultiplicator * toView.frame.width, y: 0)
}
containerView.addSubview(toView)
let animator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.9, animations: nil)
animator.addAnimations {
if self.presenting {
toView.transform = .identity
fromView.transform = CGAffineTransform(translationX: -self.movingMultiplicator * toView.frame.width, y: 0)
} else {
toView.transform = .identity
fromView.transform = CGAffineTransform(translationX: self.movingMultiplicator * toView.frame.width, y: 0)
}
}
animator.addCompletion { (position) in
// Important to set frame above (device rotation will otherwise mess things up)
toView.transform = .identity
fromView.transform = .identity
if !transitionContext.transitionWasCancelled {
self.shouldAnimateInteractively = false
}
self.animator = nil
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
self.animator = animator
return animator
}
// ====================
}
Вот часть, которая добавляет интерактивность. Это метод, который вызывается UIPanGestureRecognizer
, который я добавил к кнопке.
public lazy var transitionAnimator: MovingTransitionAnimator = MovingTransitionAnimator()
public lazy var interactionController = UIPercentDrivenInteractiveTransition()
...
@objc private func handlePan(pan: UIPanGestureRecognizer) {
let translation = pan.translation(in: utilityView)
var progress = (translation.x / utilityView.frame.width)
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch pan.state {
case .began:
// This is a flag that helps me distinguish between when a user taps on the button and when he starts a pan
transitionAnimator.shouldAnimateInteractively = true
// Just a dummy view controller that's dismissing as soon as its been presented (the problem occurs with every view controller I use here)
let vc = UIViewController()
vc.view.backgroundColor = .red
vc.transitioningDelegate = self
present(vc, animated: true, completion: {
self.transitionAnimator.shouldAnimateInteractively = false
vc.dismiss(animated: true, completion: nil)
})
case .changed:
interactionController.update(progress)
case .cancelled:
interactionController.cancel()
case .ended:
if progress > 0.55 || pan.velocity(in: utilityView).x > 600
interactionController.completionSpeed = 0.8
interactionController.finish()
} else {
interactionController.completionSpeed = 0.8
interactionController.cancel()
}
default:
break
}
}
Я также реализовал все необходимые методы делегата:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.presenting = true
return transitionAnimator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.presenting = false
return transitionAnimator
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? MovingTransitionAnimator, animator.shouldAnimateInteractively else { return nil }
return interactionController
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? MovingTransitionAnimator, animator.shouldAnimateInteractively else { return nil }
return interactionController
}
Вот и все. За этим больше нет логики (я думаю; если вам нужна дополнительная информация, пожалуйста, сообщите мне), но в ней все еще есть эта ошибка. Вот запись об ошибке. Вы не можете видеть мое прикосновение, но все, что я делаю, это касаюсь -> быстро, коротко проводя вправо -> касаясь . И после того, как этот действительно медленный переход закончился, я не могу выбросить красный view controller
. Он застрял там:
Вот что еще страннее:
Ни interactionController.finish()
, ни interactionController.cancel()
не вызывается, когда это происходит (по крайней мере, не из моего handlePan(_:)
метода).
Я проверил view hierarchy
в Xcode
после того, как произошла эта ошибка, и я получил это:
Во-первых, он, похоже, застрял в переходе (все еще внутри UITransitionView
).
Во-вторых, с левой стороны вы видите views
первого view controller
(с которого я начинаю переход). Однако на изображении виден только красный view controller
, который должен был быть представлен.
Ты хоть представляешь, что происходит? Я пытался понять это последние 3 часа, но не могу заставить его работать должным образом. Буду признателен за любую помощь
Спасибо!
EDIT
Хорошо, я нашел способ воспроизвести его 100% времени. Я также создал отдельный проект, демонстрирующий проблему (он немного отличается по структуре, потому что я пробовал много вещей, но результат все тот же)
Вот проект: https://github.com/d3mueller/InteractiveTransitionDemo2
Как воспроизвести проблему:
Проведите справа налево, а затем быстро слева направо. Это вызовет ошибку.
Также, похожая ошибка появится, когда вы проведете справа налево очень быстро несколько раз. Затем он фактически запустит переход и завершит его правильно (но он даже не должен начинаться, потому что при движении справа налево значение progress
остается равным 0,0)