调用 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 之间),则不会更新安全区域
在我看来,在调用拥有视图控制器的 .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 之间),则不会更新安全区域