调用 UIViewController.viewWillDisappear 后安全区域未更新

Safe area is not updated after UIViewController.viewWillDisappear is called

在我看来,在调用拥有视图控制器的 .viewWillDisappear() 方法后,视图的安全区域似乎没有更新。

这是有意为之还是框架中的错误?

通过创建一个自定义 UIViewControllerTransitioningDelegate 可以很容易地看到这个问题,该自定义 UIViewControllerTransitioningDelegate 在一个视图控制器中为较小的视图设置动画,在另一个视图控制器中设置为全屏大小(限制在安全区域)。然后安全区域将随着当前动画的进行而扩大(如预期的那样),但不会随着关闭动画的进行而缩小(不是预期的!)。预期的行为是安全区域在当前动画期间扩大,并在关闭动画期间缩小。

下面的 gif 显示了意外行为。呈现的视图控制器的灰色区域是安全区域。

下面是我用来可视化这个问题的代码。 ViewController.swift 使用 FullScreenTransitionManager.swift

呈现 MyViewController.swift
//
//  FullScreenTransitionManager.swift
//

import Foundation
import UIKit

// MARK: FullScreenPresentationController

final class FullScreenPresentationController: UIPresentationController {
    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)
    }
    
    override func presentationTransitionDidEnd(_ completed: Bool) {
        if !completed {
            containerView?.removeGestureRecognizer(tapGestureRecognizer)
        }
    }
    
    override func dismissalTransitionDidEnd(_ completed: Bool) {
        if completed {
            containerView?.removeGestureRecognizer(tapGestureRecognizer)
        }
    }
}

// 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?.frame ?? CGRect(origin: presented.view.center, size: .zero)
        return FullScreenAnimationController(animationType: .present,
                                             anchorFrame: anchorFrame)
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        let anchorFrame = anchorView?.frame ?? 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 = 5) {
        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)
        viewController.view.frame = anchorFrame
        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(with transitionContext: UIViewControllerContextTransitioning,
                                 animating viewController: UIViewController) -> UIViewPropertyAnimator {
        return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext),
                                                              delay: 0,
                                                              options: [.curveEaseInOut],
                                                              animations: {
            viewController.view.frame = self.anchorFrame
            viewController.view.layoutIfNeeded()
        }, completion: { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }
}
//
//  MyViewController.swift
//

import UIKit

class MyViewController: UIViewController {
    private let square: UIView = {
        let view = UIView()
        view.backgroundColor = .white
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemGray
        
        view.addSubview(square)
        
        NSLayoutConstraint.activate([
            square.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            square.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            square.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            square.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ])
    }
}
//
//  ViewController.swift
//

import UIKit

class ViewController: UIViewController {
    private let button: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Click Me!", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.backgroundColor = .white
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    private var fullScreenTransitionManager: FullScreenTransitionManager?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.widthAnchor.constraint(equalToConstant: 200),
            button.heightAnchor.constraint(equalToConstant: 200),
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        
        button.addTarget(self, action: #selector(presentMyViewController), for: .primaryActionTriggered)
    }

    @objc private func presentMyViewController(_ button: UIButton) {
        let fullScreenTransitionManager = FullScreenTransitionManager(anchorView: button)
        let myViewController = MyViewController()
        myViewController.modalPresentationStyle = .custom
        myViewController.transitioningDelegate = fullScreenTransitionManager
        present(myViewController, animated: true)
        self.fullScreenTransitionManager = fullScreenTransitionManager
    }
}

正如 this post on the Apple Developer forums 上的答案所证实的那样,这种行为是预期的。如果视图控制器未处于出现状态(在 viewWillAppear 和 viewWillDisappear 之间),则不会更新安全区域