使用自定义 UIViewControllerTransitioningDelegate 在视图控制器关闭期间不考虑 additionalSafeAreaInsets
additionalSafeAreaInsets is not accounted for during view controller dismissal, using custom UIViewControllerTransitioningDelegate
所以,直奔问题:
我创建了一个自定义 UIViewControllerTransitioningDelegate,我用它来为来自一个视图控制器的视图设置动画,以在另一个视图控制器中全屏显示。我通过创建 UIViewControllerAnimatedTransitioning 对象来实现这一点,这些对象为呈现的视图的框架设置动画。而且效果很好!除非我在解雇期间尝试调整拥有视图的视图控制器的 additionalSafeAreaInsets...
当我尝试为视图控制器及其视图的关闭设置动画时,似乎没有考虑到 属性。它在演示期间工作正常。
下面的 gif 显示了它的外观。红色框是呈现视图的安全区域(加上一些填充) - 我试图在动画期间使用视图控制器的 additionalSafeAreaInsets 属性 来补偿拥有视图。
如 gif 所示,安全区域在演示期间已正确调整,但在解散期间未正确调整。
所以,我想要的是:使用 additionalSafeAreaInsets 通过将 additionalSafeAreaInsets 设置为安全区域的“反转”值。因此,有效安全区域从 0 开始并在呈现期间“动画化”到预期值,并从预期值开始并在解散期间“动画化”到 0。
(我引用的是“动画”,因为它实际上是动画视图的帧。但是 UIKit/Auto 布局在计算帧时使用这些属性)
非常欢迎任何关于如何解决这个问题的想法!
下面提供了自定义 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)
})
}
}
经过一些调试后,我设法找到了解决此问题的方法。
简而言之,看起来安全区域在调用 UIViewController.viewWillDisappear
后没有更新,因此对 .additionalSafeAreaInsets
的任何更改都将被忽略(因为这些插图修改了视图控制器的安全区域查看)。
我当前的解决方法有点老套,但可以完成工作。由于 UIViewControllerTransitioningDelegate.animationController(forDismissed...)
在 UIViewController.viewWillDisappear
和 UIViewControllerAnimatedTransitioning.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
}
}
此外,这里是 Apple 论坛上的 regarding the safe area not updating after UIViewController.viewWillDisappear
. And a link to a similar post。
所以,直奔问题:
我创建了一个自定义 UIViewControllerTransitioningDelegate,我用它来为来自一个视图控制器的视图设置动画,以在另一个视图控制器中全屏显示。我通过创建 UIViewControllerAnimatedTransitioning 对象来实现这一点,这些对象为呈现的视图的框架设置动画。而且效果很好!除非我在解雇期间尝试调整拥有视图的视图控制器的 additionalSafeAreaInsets...
当我尝试为视图控制器及其视图的关闭设置动画时,似乎没有考虑到 属性。它在演示期间工作正常。
下面的 gif 显示了它的外观。红色框是呈现视图的安全区域(加上一些填充) - 我试图在动画期间使用视图控制器的 additionalSafeAreaInsets 属性 来补偿拥有视图。
如 gif 所示,安全区域在演示期间已正确调整,但在解散期间未正确调整。
所以,我想要的是:使用 additionalSafeAreaInsets 通过将 additionalSafeAreaInsets 设置为安全区域的“反转”值。因此,有效安全区域从 0 开始并在呈现期间“动画化”到预期值,并从预期值开始并在解散期间“动画化”到 0。 (我引用的是“动画”,因为它实际上是动画视图的帧。但是 UIKit/Auto 布局在计算帧时使用这些属性)
非常欢迎任何关于如何解决这个问题的想法!
下面提供了自定义 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)
})
}
}
经过一些调试后,我设法找到了解决此问题的方法。
简而言之,看起来安全区域在调用 UIViewController.viewWillDisappear
后没有更新,因此对 .additionalSafeAreaInsets
的任何更改都将被忽略(因为这些插图修改了视图控制器的安全区域查看)。
我当前的解决方法有点老套,但可以完成工作。由于 UIViewControllerTransitioningDelegate.animationController(forDismissed...)
在 UIViewController.viewWillDisappear
和 UIViewControllerAnimatedTransitioning.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
}
}
此外,这里是 Apple 论坛上的 UIViewController.viewWillDisappear
. And a link to a similar post。