交互式关闭模态视图控制器时出现故障。

7

我正在使用UIViewControllerAnimatedTransitioningUIPercentDrivenInteractiveTransition来交互式地取消模态呈现的视图控制器。没有太多花哨的东西。但是我注意到在交互开始时偶尔会出现小故障。如果使用.curveEaseOut选项进行动画处理,则会更加明显。在我跟随一些在线教程(https://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/)时也会出现同样的问题。当我第一次向下拖动它时,您可以在 gif 中看到故障。有什么建议吗?

enter image description here

MyDismissAnimator

class SlideInDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    // ...

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        guard let toVC = transitionContext.viewController(forKey: .to), 
        let presentedVC = transitionContext.viewController(forKey: .from) else {return}

        let presentedFrame = transitionContext.finalFrame(for: presentedVC)
        var dismissedFrame = presentedFrame
        dismissedFrame.origin.y = transitionContext.containerView.frame.size.height

        transitionContext.containerView.insertSubview(toVC.view, belowSubview: presentedVC.view)
        }
        let duration = transitionDuration(using: transitionContext)

        UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
            presentedVC.view.frame = dismissedFrame
        }) { _ in
            if transitionContext.transitionWasCancelled {
                toVC.view.removeFromSuperview()
            }
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    }
}

MyInteractor

class SwipeInteractionController: UIPercentDrivenInteractiveTransition {

    var interactionInProgress = false
    private var shouldCompleteTransition = false
    private weak var viewController: UIViewController!

    init(viewController: UIViewController) {
        self.viewController = viewController
        super.init()
        let gesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
        viewController.view?.addGestureRecognizer(gesture)
    }

    @objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
        let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
        var progress = (translation.y / viewController.view.bounds.height)
        progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))

        switch gestureRecognizer.state {
        case .began:
            interactionInProgress = true
            viewController.dismiss(animated: true, completion: nil)

        case .changed:
            shouldCompleteTransition = progress > 0.3
            update(progress)

        case .cancelled:
            interactionInProgress = false
            cancel()

        case .ended:
            interactionInProgress = false
            if shouldCompleteTransition {
                finish()
            } else {
                cancel()
            }
        default:
            break
        }
    }
}

1
你是在后台线程调用 animateTransition 函数吗?能否确保它从主线程调用? - mfaani
@Honey,animateTransition在dismiss动画器中,由UIKit自动调用。所以我猜这不是问题所在?让我知道你认为应该如何处理。 - Jack Guo
哈哈,我以为那是你自己写的函数。我的错。不,这不是问题。只是确认一下:你没有在 Xcode 上启用慢动画吧?为了避免这种情况,请前往模拟器 >> 调试 >> 慢动画... - mfaani
不,我现在甚至在Xcode中都找不到那个选项。 - Jack Guo
显示剩余2条评论
5个回答

10

我相信这是UIPercentDrivenInteractiveTransition的一个bug,但我已经成功解决了:

当dismiss()被调用后,进度没有尽快更新时,就会出现这个bug。这是因为平移手势识别器的.changed状态只有在拖动时才被触发。因此,如果您拖动得很慢,在调用.begin和第一次调用.changed之间的时间内,dismiss转换将开始动画。

您可以通过在视图上缓慢地像素点一样地拖动,直到调用.begin,来在模拟器中看到这一点。只要.changed再次没有被调用,转换实际上将完成自己,并且您将看到视图完全向下动画并完成转换。

然而,仅在.dismiss()后调用update(progress)也不起作用,我认为是因为dismiss动画还没有开始。(直到下一个runloop或其他什么东西)

我的“hack”解决方案是使用非常短的异步调度时间,有效地设置进度并在动画开始之前停止它。

case .began:
    interactionInProgress = true
    viewController.dismiss(animated: true, completion: nil)
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) {
        self.update(progress)
    }

2

1
如果您使用自定义的dismissal转场动画器,则不应将toViewController的视图添加为transitionContext的容器视图的子视图。它已经在层次结构中了。这个bug在我的同一个案例中发生了,但我同时使用了dismissal和appear自定义动画。我认为仅在自定义dismissal动画中也会出现这种情况。
删除此行:

transitionContext.containerView.insertSubview(toVC.view, belowSubview: presentedVC.view)

你只需要做的就是在动画块中更改fromViewController的框架即可。

嗨 @Sergey。问题变得更糟了,每次开始交互时都会出现故障。而且,在过渡期间背景完全变黑,然后在完成时突然恢复... - Jack Guo
@JGuo,好的,我错了,以为它已经在层次结构中了。如果您禁用交互式动画,会发生什么情况?如果故障不会出现-这不是交互式动画的问题。 还有一个想法:可能是您呈现的VC存在问题(布局/框架大小,如果它通过viewWillAppear中的某些约束动态更改),因为如果您的交互式动画调整特殊方式,它可能会调用手势识别器的“取消”状态。这将调用您要解除的VC的viewWillAppear,并且它可能会跳跃,因为VWA中的框架与解除动画代码中的框架不同。只是个想法。 - Sergey Fedorov
如果我不进行任何交互,一切都能完美运行。不,它只是一个普通的 UIViewController,除了 backgroundColor = .white 之外没有其他东西。 - Jack Guo
@JGuo,请附上交互式转换控制器的代码。 - Sergey Fedorov
添加了一个相当标准的实现,没有什么花里胡哨的东西。 - Jack Guo
@JGuo 尝试在动画中添加 UIViewAnimationOptionAllowAnimatedContent 选项。我在这些文件中没有看到实际的问题,也许这会有所帮助。 - Sergey Fedorov

0

我遇到了完全相同的问题,即在被UIPercentDrivenInteractiveTransition控制之前,“自动”启动了dismiss动画(忽略任何交互)。然后我想,那些最初的毫秒中发生的dismiss动画实际上是由你的实现提供的自定义动画:

animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?

所以你可以做的是,增加自定义动画的持续时间,比如10秒钟,这样初始抖动就不会再发生了。 为了让“后交互”动画以正常速度进行(即当您放开视图控制器时),您可以使用以下代码行来增加UIPercentDrivenInteractiveTransition实例的完成速度:
percentDrivenTransition.completionSpeed = 10.0

如果没有它,剩下的动画需要几秒钟才能完成,这不是我们想要的。

希望这有所帮助。


0

我通过“拥抱”故障来解决了同样的问题。 问题在于旧视图在交互器启动之前向下移动了一点。然后控制权交给了重置位置并将其移回动画开头的交互器。 如果交互器在控制权被交给它时从旧视图所在的位置开始动画,那么几乎看不到故障。

为了做到这一点,我从UIView.animate中删除了缓动曲线参数,并在调用update(progress)之前对进度应用了自己的缓动函数。

我的自定义缓动函数有一个偏移参数,用于补偿交互器接管之前发生的移动。

您必须根据应用的过渡持续时间找到正确的偏移参数量。对于0.3秒的过渡持续时间,最佳偏移量为0.05。

您可以在此处下载一个可工作的示例:https://github.com/francosolerio/InteractiveModal

缓动函数

struct Curves {
    static func quadraticEaseIn<T: FloatingPoint>(t: T) -> T {
        return t * t
    }

    static func quadraticEaseOut<T: FloatingPoint>(t: T) -> T {
        return 1 - quadraticEaseIn(t: 1 - t)
    }

    static func QuadraticEaseInOut<T: FloatingPoint>(t: T) -> T {
        if t < 1/2 {
            return quadraticEaseIn(t: t * 2) / 2
        } else {
            return 1 - quadraticEaseIn(t: (1 - t) * 2) / 2
        }
    }

    static func quadraticEaseOut<T: FloatingPoint>(t: T, offset: T) -> T {
        return quadraticEaseOut(t: t + offset)
    }    
}

动画师

UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                fromVC.view.frame = finalFrame
            },
            completion: { _ in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            }
        )

交互器

var progress = Curves.quadraticEaseOut(t: translation.y / viewController.view.bounds.height, offset: 0.05)
        progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接