tho*_*med 5 animation uikit ios autolayout swift
那么,直接说问题吧:
我创建了一个自定义UIViewControllerTransitioningDelegate,用于将一个视图控制器中的视图动画化为另一个视图控制器中的全屏。我通过创建UIViewControllerAnimatedTransitioning对象来实现此目的,这些对象可以对所呈现的视图框架进行动画处理。而且效果很好!除非我尝试在解雇期间调整拥有视图的视图控制器的additionalSafeAreaInsets ...
当我尝试以动画方式关闭视图控制器及其视图时,似乎没有考虑到此属性。它在演示过程中运行良好。
下面的 gif 显示了它的外观。红色框是所呈现视图的安全区域(加上一些填充) - 我试图在动画期间使用拥有该视图的视图控制器的additionalSafeAreaInsets属性来补偿它。

如图 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)
经过一些调试后,我设法找到了解决此问题的方法。
简而言之,看起来安全区域在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 论坛上类似帖子的链接。