使用自定义 UIViewControllerTransitioningDelegate 在视图控制器解除期间不考虑 extraSafeAreaInsets

tho*_*med 5 animation uikit ios autolayout swift

那么,直接说问题吧:

我创建了一个自定义UIViewControllerTransitioningDelegate,用于将一个视图控制器中的视图动画化为另一个视图控制器中的全屏。我通过创建UIViewControllerAnimatedTransitioning对象来实现此目的,这些对象可以对所呈现的视图框架进行动画处理。而且效果很好!除非我尝试在解雇期间调整拥​​有视图的视图控制器的additionalSafeAreaInsets ...

当我尝试以动画方式关闭视图控制器及其视图时,似乎没有考虑到此属性。它在演示过程中运行良好。

下面的 gif 显示了它的外观。红色框是所呈现视图的安全区域(加上一些填充) - 我试图在动画期间使用拥有该视图的视图控制器的additionalSafeAreaInsets属性来补偿它。

extraSafeAreaInsets 仅在演示期间考虑

如图 gif 所示,安全区域在演示过程中得到了适当调整,但在解散过程中没有进行适当调整。

所以,我想要的是:通过将additionalSafeAreaInsets设置为安全区域的“反转”值,使用additionalSafeAreaInsets来减少动画期间安全区域的影响。因此,有效安全区域在演示期间从 0 开始并“动画”到预期值,在关闭期间从预期值开始并“动画”到 0。(我引用“animates”,因为它实际上是动画视图的框架。但是 UIKit/Auto Layout 在计算框架时使用这些属性)

非常欢迎任何关于如何解决这个问题的想法!

下面提供了自定义 UIViewControllerTransitioningDelegate 的代码。

//
//  FullScreenTransitionManager.swift
//

import Foundation
import UIKit

// MARK: FullScreenPresentationController

final class FullScreenPresentationController: UIPresentationController {
    private let backgroundView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemBackground
        view.alpha = 0
        return view
    }()
    
    private lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap))
    
    @objc private func onTap(_ gesture: UITapGestureRecognizer) {
        presentedViewController.dismiss(animated: true)
    }
}
    
// MARK: UIPresentationController
    
extension FullScreenPresentationController {
    override func presentationTransitionWillBegin() {
        guard let containerView = containerView else { return }
        
        containerView.addGestureRecognizer(tapGestureRecognizer)
        
        containerView.addSubview(backgroundView)
        backgroundView.frame = containerView.frame
        
        guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
        
        transitionCoordinator.animate(alongsideTransition: { context in
            self.backgroundView.alpha = 1
        })
    }
    
    override func presentationTransitionDidEnd(_ completed: Bool) {
        if !completed {
            backgroundView.removeFromSuperview()
            containerView?.removeGestureRecognizer(tapGestureRecognizer)
        }
    }
    
    override func dismissalTransitionWillBegin() {
        guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
        
        transitionCoordinator.animate(alongsideTransition: { context in
            self.backgroundView.alpha = 0
        })
    }
    
    override func dismissalTransitionDidEnd(_ completed: Bool) {
        if completed {
            backgroundView.removeFromSuperview()
            containerView?.removeGestureRecognizer(tapGestureRecognizer)
        }
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        guard
            let containerView = containerView,
            let presentedView = presentedView
        else { return }
        coordinator.animate(alongsideTransition: { context in
            self.backgroundView.frame = containerView.frame
            presentedView.frame = self.frameOfPresentedViewInContainerView
        })
    }
}

// MARK: FullScreenTransitionManager

final class FullScreenTransitionManager: NSObject, UIViewControllerTransitioningDelegate {
    private weak var anchorView: UIView?
    
    init(anchorView: UIView) {
        self.anchorView = anchorView
    }
    
    func presentationController(forPresented presented: UIViewController,
                                presenting: UIViewController?,
                                source: UIViewController) -> UIPresentationController? {
        FullScreenPresentationController(presentedViewController: presented, presenting: presenting)
    }
    
    func animationController(forPresented presented: UIViewController,
                             presenting: UIViewController,
                             source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        let anchorFrame = anchorView?.safeAreaLayoutGuide.layoutFrame ?? CGRect(origin: presented.view.center, size: .zero)
        return FullScreenAnimationController(animationType: .present,
                                             anchorFrame: anchorFrame)
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        let anchorFrame = anchorView?.safeAreaLayoutGuide.layoutFrame ?? CGRect(origin: dismissed.view.center, size: .zero)
        return FullScreenAnimationController(animationType: .dismiss,
                                             anchorFrame: anchorFrame)
    }
}

// MARK: UIViewControllerAnimatedTransitioning

final class FullScreenAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
    enum AnimationType {
        case present
        case dismiss
    }
    
    private let animationType: AnimationType
    private let anchorFrame: CGRect
    private let animationDuration: TimeInterval
    private var propertyAnimator: UIViewPropertyAnimator?
    
    init(animationType: AnimationType, anchorFrame: CGRect, animationDuration: TimeInterval = 0.3) {
        self.animationType = animationType
        self.anchorFrame = anchorFrame
        self.animationDuration = animationDuration
    }
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        animationDuration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        switch animationType {
        case .present:
            guard
                let toViewController = transitionContext.viewController(forKey: .to)
            else {
                return transitionContext.completeTransition(false)
            }
            transitionContext.containerView.addSubview(toViewController.view)
            propertyAnimator = presentAnimator(with: transitionContext, animating: toViewController)
        case .dismiss:
            guard
                let fromViewController = transitionContext.viewController(forKey: .from)
            else {
                return transitionContext.completeTransition(false)
            }
            propertyAnimator = dismissAnimator(with: transitionContext, animating: fromViewController)
        }
    }
    
    private func presentAnimator(with transitionContext: UIViewControllerContextTransitioning,
                                 animating viewController: UIViewController) -> UIViewPropertyAnimator {
        let finalFrame = transitionContext.finalFrame(for: viewController)
        let safeAreaInsets = transitionContext.containerView.safeAreaInsets
        let safeAreaCompensation = UIEdgeInsets(top: -safeAreaInsets.top,
                                                left: -safeAreaInsets.left,
                                                bottom: -safeAreaInsets.bottom,
                                                right: -safeAreaInsets.right)
        viewController.additionalSafeAreaInsets = safeAreaCompensation
        viewController.view.frame = anchorFrame
        viewController.view.setNeedsLayout()
        viewController.view.layoutIfNeeded()
        return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut, .layoutSubviews], animations: {
            viewController.additionalSafeAreaInsets = .zero
            viewController.view.frame = finalFrame
            viewController.view.setNeedsLayout()
            viewController.view.layoutIfNeeded()
        }, completion: { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }
    
    private func dismissAnimator(with transitionContext: UIViewControllerContextTransitioning,
                                 animating viewController: UIViewController) -> UIViewPropertyAnimator {
        let finalFrame = anchorFrame
        let safeAreaInsets = transitionContext.containerView.safeAreaInsets
        let safeAreaCompensation = UIEdgeInsets(top: -safeAreaInsets.top,
                                                left: -safeAreaInsets.left,
                                                bottom: -safeAreaInsets.bottom,
                                                right: -safeAreaInsets.right)
        viewController.view.setNeedsLayout()
        viewController.view.layoutIfNeeded()
        return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut, .layoutSubviews], animations: {
            viewController.additionalSafeAreaInsets = safeAreaCompensation
            viewController.view.frame = finalFrame
            viewController.view.setNeedsLayout()
            viewController.view.layoutIfNeeded()
        }, completion: { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }
}

Run Code Online (Sandbox Code Playgroud)

tho*_*med 0

经过一些调试后,我设法找到了解决此问题的方法。

简而言之,看起来安全区域在UIViewController.viewWillDisappear调用后没有更新,因此任何更改都会.additionalSafeAreaInsets被忽略(因为这些插入修改了视图控制器视图的安全区域)。

我目前的解决方法有点老套,但它可以完成工作。由于在和UIViewControllerTransitioningDelegate.animationController(forDismissed...)之前调用,因此我已经在该方法中启动了关闭动画。这样动画的布局计算就会正确,并设置正确的安全区域。UIViewController.viewWillDisappearUIViewControllerAnimatedTransitioning.animateTransition(using transitionContext...)

下面是我的自定义代码UIViewControllerTransitioningDelegate以及解决方法。注意:我已经删除了它的使用,.additionalSafeAreaInsets因为它根本没有必要!我不知道为什么我认为我首先需要它......

//
//  FullScreenTransitionManager.swift
//

import Foundation
import UIKit

// MARK: FullScreenPresentationController

final class FullScreenPresentationController: UIPresentationController {
    private let backgroundView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemBackground
        view.alpha = 0
        return view
    }()
    
    private lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap))
    
    @objc private func onTap(_ gesture: UITapGestureRecognizer) {
        presentedViewController.dismiss(animated: true)
    }
}
    
// MARK: UIPresentationController
    
extension FullScreenPresentationController {
    override func presentationTransitionWillBegin() {
        guard let containerView = containerView else { return }
        
        containerView.addGestureRecognizer(tapGestureRecognizer)
        
        containerView.addSubview(backgroundView)
        backgroundView.frame = containerView.frame
        
        guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
        
        transitionCoordinator.animate(alongsideTransition: { context in
            self.backgroundView.alpha = 1
        })
    }
    
    override func presentationTransitionDidEnd(_ completed: Bool) {
        if !completed {
            backgroundView.removeFromSuperview()
            containerView?.removeGestureRecognizer(tapGestureRecognizer)
        }
    }
    
    override func dismissalTransitionWillBegin() {
        guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
        
        transitionCoordinator.animate(alongsideTransition: { context in
            self.backgroundView.alpha = 0
        })
    }
    
    override func dismissalTransitionDidEnd(_ completed: Bool) {
        if completed {
            backgroundView.removeFromSuperview()
            containerView?.removeGestureRecognizer(tapGestureRecognizer)
        }
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        guard let containerView = containerView else { return }
        coordinator.animate(alongsideTransition: { context in
            self.backgroundView.frame = containerView.frame
        })
    }
}

// MARK: FullScreenTransitionManager

final class FullScreenTransitionManager: NSObject, UIViewControllerTransitioningDelegate {
    fileprivate enum AnimationState {
        case present
        case dismiss
    }
    
    private weak var anchorView: UIView?
    
    private var animationState: AnimationState = .present
    private var animationDuration: TimeInterval = Resources.animation.duration
    private var anchorViewFrame: CGRect = .zero
    
    private var propertyAnimator: UIViewPropertyAnimator?
    
    init(anchorView: UIView) {
        self.anchorView = anchorView
    }
    
    func presentationController(forPresented presented: UIViewController,
                                presenting: UIViewController?,
                                source: UIViewController) -> UIPresentationController? {
        FullScreenPresentationController(presentedViewController: presented, presenting: presenting)
    }
    
    func animationController(forPresented presented: UIViewController,
                             presenting: UIViewController,
                             source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        prepare(animationState: .present)
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        // Starting the animation here, since UIKit do not update safe area insets after UIViewController.viewWillDisappear() is called
        defer {
            propertyAnimator = dismissAnimator(animating: dismissed)
        }
        return prepare(animationState: .dismiss)
    }
}

// MARK: UIViewControllerAnimatedTransitioning

extension FullScreenTransitionManager: UIViewControllerAnimatedTransitioning {
    private func prepare(animationState: AnimationState,
                         animationDuration: TimeInterval = Resources.animation.duration) -> UIViewControllerAnimatedTransitioning? {
        guard let anchorView = anchorView else { return nil }
        
        self.animationState = animationState
        self.animationDuration = animationDuration
        self.anchorViewFrame = anchorView.safeAreaLayoutGuide.layoutFrame
        
        return self
    }
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        animationDuration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        switch animationState {
        case .present:
            guard
                let toViewController = transitionContext.viewController(forKey: .to)
            else {
                return transitionContext.completeTransition(false)
            }
            propertyAnimator = presentAnimator(with: transitionContext, animating: toViewController)
        case .dismiss:
            guard
                let fromViewController = transitionContext.viewController(forKey: .from)
            else {
                return transitionContext.completeTransition(false)
            }
            propertyAnimator = updatedDismissAnimator(with: transitionContext, animating: fromViewController)
        }
    }
    
    private func presentAnimator(with transitionContext: UIViewControllerContextTransitioning,
                                 animating viewController: UIViewController) -> UIViewPropertyAnimator {
        transitionContext.containerView.addSubview(viewController.view)
        let finalFrame = transitionContext.finalFrame(for: viewController)
        viewController.view.frame = anchorViewFrame
        viewController.view.layoutIfNeeded()
        return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext),
                                                              delay: 0,
                                                              options: [.curveEaseInOut],
                                                              animations: {
            viewController.view.frame = finalFrame
            viewController.view.layoutIfNeeded()
        }, completion: { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }
    
    private func dismissAnimator(animating viewController: UIViewController) -> UIViewPropertyAnimator {
        let finalFrame = anchorViewFrame
        return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: animationDuration,
                                                              delay: 0,
                                                              options: [.curveEaseInOut],
                                                              animations: {
            viewController.view.frame = finalFrame
            viewController.view.layoutIfNeeded()
        })
    }
    
    private func updatedDismissAnimator(with transitionContext: UIViewControllerContextTransitioning,
                                        animating viewController: UIViewController) -> UIViewPropertyAnimator {
        let propertyAnimator = self.propertyAnimator ?? dismissAnimator(animating: viewController)
        propertyAnimator.addCompletion({ _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
        self.propertyAnimator = propertyAnimator
        return propertyAnimator
    }
}
Run Code Online (Sandbox Code Playgroud)

另外,这里还有一个 Stack Overflow 帖子的链接,内容涉及安全区域在 后不会更新UIViewController.viewWillDisappear。以及Apple 论坛上类似帖子的链接。