使用自定义 UIViewControllerTransitioningDelegate,View 的帧在关闭动画之前跳转
View’s frame jumps right before dismiss animation, using custom UIViewControllerTransitioningDelegate
那么,开门见山:
我正在使用自定义的 UIViewControllerTransitioningDelegate,它提供自定义的 UIPresentationController 和 present/dismiss 动画来为视图设置动画从一个视图控制器到另一个。当在第一个视图控制器的 table 视图单元格中粘贴图像时,图像在第二个视图控制器中以全屏显示,从它在 table 视图单元格中的位置到它在呈现的视图控制器。
下面的 gif 显示了正在发生的事情。请注意,当前动画一切正常,但关闭动画则不然。
我遇到的问题是,当关闭动画触发时,看起来动画视图的框架在某种程度上发生了偏移或变形。我不知道为什么!动画开始时的帧未被触及(至少我是这样),动画结束时的帧与当前动画的帧相同 - 效果非常好!
有人知道发生了什么事吗?
下面提供了我的自定义 UIViewControllerTransitioningDelegate 的代码。
//
// FullScreenTransitionManager.swift
//
import Foundation
import UIKit
// MARK: FullScreenPresentationController
final class FullScreenPresentationController: UIPresentationController {
private lazy var backgroundView: UIVisualEffectView = {
let blurVisualEffectView = UIVisualEffectView(effect: blurEffect)
blurVisualEffectView.effect = nil
return blurVisualEffectView
}()
private let blurEffect = UIBlurEffect(style: .systemThinMaterial)
private lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap))
@objc private func onTap(_ gesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true)
}
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.effect = self.blurEffect
})
}
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.effect = nil
})
}
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? {
guard let anchorView = anchorView, let anchorViewSuperview = anchorView.superview else { return nil }
let anchorViewFrame = CGRect(origin: anchorViewSuperview.convert(anchorView.frame.origin, to: nil), size: anchorView.frame.size)
let anchorViewTag = anchorView.tag
return FullScreenAnimationController(animationType: .present, anchorViewFrame: anchorViewFrame, anchorViewTag: anchorViewTag)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let anchorView = anchorView, let anchorViewSuperview = anchorView.superview else { return nil }
let anchorViewFrame = CGRect(origin: anchorViewSuperview.convert(anchorView.frame.origin, to: nil), size: anchorView.frame.size)
let anchorViewTag = anchorView.tag
return FullScreenAnimationController(animationType: .dismiss, anchorViewFrame: anchorViewFrame, anchorViewTag: anchorViewTag)
}
}
// MARK: UIViewControllerAnimatedTransitioning
final class FullScreenAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum AnimationType {
case present
case dismiss
}
private let animationType: AnimationType
private let anchorViewFrame: CGRect
private let anchorViewTag: Int
private let animationDuration: TimeInterval
private var propertyAnimator: UIViewPropertyAnimator?
init(animationType: AnimationType, anchorViewFrame: CGRect, anchorViewTag: Int, animationDuration: TimeInterval = 0.3) {
self.animationType = animationType
self.anchorViewFrame = anchorViewFrame
self.anchorViewTag = anchorViewTag
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 window = transitionContext.containerView.window!
let finalRootViewFrame = transitionContext.finalFrame(for: viewController)
viewController.view.frame = finalRootViewFrame
viewController.view.setNeedsUpdateConstraints()
viewController.view.setNeedsLayout()
viewController.view.layoutIfNeeded()
let view: UIView = viewController.view.viewWithTag(anchorViewTag) ?? viewController.view
let finalFrame = view.frame
view.frame = CGRect(origin: window.convert(anchorViewFrame.origin, to: view.superview!), size: anchorViewFrame.size)
view.setNeedsUpdateConstraints()
view.setNeedsLayout()
view.layoutIfNeeded()
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut], animations: {
view.frame = finalFrame
view.setNeedsUpdateConstraints()
view.setNeedsLayout()
view.layoutIfNeeded()
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
private func dismissAnimator(with transitionContext: UIViewControllerContextTransitioning,
animating viewController: UIViewController) -> UIViewPropertyAnimator {
let window = transitionContext.containerView.window!
let view: UIView = viewController.view.viewWithTag(anchorViewTag) ?? viewController.view
let finalFrame = CGRect(origin: window.convert(anchorViewFrame.origin, to: view.superview!), size: anchorViewFrame.size)
viewController.view.setNeedsUpdateConstraints()
viewController.view.setNeedsLayout()
viewController.view.layoutIfNeeded()
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut], animations: {
view.frame = finalFrame
view.setNeedsUpdateConstraints()
view.setNeedsLayout()
view.layoutIfNeeded()
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
同时为我的 FullScreenImageViewController 添加代码。
//
// FullScreenImageViewController.swift
//
import UIKit
import TinyConstraints
class FullScreenImageViewController: UIViewController {
private let imageView: UIImageView = {
let image = UIImage(named: "Bananas")!
let imageView = UIImageView(image: image)
let aspectRatio = imageView.intrinsicContentSize.width / imageView.intrinsicContentSize.height
imageView.contentMode = .scaleAspectFit
imageView.widthToHeight(of: imageView, multiplier: aspectRatio)
return imageView
}()
private lazy var imageViewWidthConstraint = imageView.widthToSuperview(relation: .equalOrLess)
private lazy var imageViewHeightConstraint = imageView.heightToSuperview(relation: .equalOrLess)
init(tag: Int) {
super.init(nibName: nil, bundle: nil)
imageView.tag = tag
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
traitCollectionChanged(from: previousTraitCollection)
}
private func configureUI() {
view.backgroundColor = .clear
view.addSubview(imageView)
imageView.centerInSuperview()
traitCollectionChanged(from: nil)
}
private func traitCollectionChanged(from previousTraitCollection: UITraitCollection?) {
if traitCollection.horizontalSizeClass != .compact {
// Landscape
imageViewWidthConstraint.isActive = false
imageViewHeightConstraint.isActive = true
} else {
// Portrait
imageViewWidthConstraint.isActive = true
imageViewHeightConstraint.isActive = false
}
}
}
以及实际呈现 FullScreenImageViewController 的代码(只是为了更好的衡量)
//
// ViewController.swift
//
import UIKit
class ViewController: UITableViewController {
// ...
// ...
private var fullScreenTransitionManager: FullScreenTransitionManager?
// ...
// ...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? TableViewCell else { return }
let fullScreenTransitionManager = FullScreenTransitionManager(anchorView: cell.bananaImageView)
let viewController = FullScreenImageViewController(tag: cell.bananaImageView.tag)
viewController.modalPresentationStyle = .custom
viewController.transitioningDelegate = fullScreenTransitionManager
present(viewController, animated: true)
self.fullScreenTransitionManager = fullScreenTransitionManager
}
}
玩了半天终于搞明白了。
而且我想我一直对问题的根源有一种感觉...
简答:
使用自动布局约束时,不要尝试通过更改 .frame
或 .bounds
来为视图设置动画。更改这些属性可能会导致未定义的行为(就像我遇到的那样)。相反,通过更改约束或 .center
and/or .transform
属性 来动画视图。这些属性不与布局引擎冲突。查询视图的大小时,请使用 .bounds
属性,因为在使用自动布局约束时此 属性 比 .frame
更可靠。
稍微长一点的回答:
由于我到处都在使用自动布局约束,因此将它与在动画期间手动更改视图帧相结合是行不通的。或者更正确 - 有未定义的行为。由于自动布局引擎使用约束为您修改视图的框架,因此您应该避免自己触摸 .frame
(和 .bounds
)属性。相反,通过更改 .center
和 .transform
等属性来为您的视图添加动画效果。这些属性似乎与自动布局不冲突,并且在自动布局引擎完成计算后,对这些属性的更改将应用于您的视图。事件想法更改视图的 .frame
和 .bounds
有时可能会与自动布局约束结合使用,就像我体验自定义演示动画一样(它似乎完美无缺!),你真的应该避免它.在某些情况下,一个解决方法可能是临时转向 .translatesAutoresizingMaskIntoConstraints == true
,但这真的不是一个好主意,因为它会导致 UIKit 为您生成自动布局约束,而这些约束可能会与您自己的约束冲突。查询视图的大小时,请使用 .bounds
属性,因为在使用自动布局约束和 .transform
[时,此 属性 比 .frame
更可靠=63=].
Apple 文档中值得一提的地方:
UIView.center
:
Use this property, instead of the frame property, when you want to change the position of a view. The center point is always valid, even when scaling or rotation factors are applied to the view's transform. Changes to this property can be animated.
UIView.transform
:
In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame.
Warning:
When the value of this property is anything other than the identity transform, the value in the frame property is undefined and should be ignored.
UIView.translatesAutoresizingMaskIntoConstraints
:
If this property’s value is true, the system creates a set of constraints that duplicate the behavior specified by the view’s autoresizing mask. This also lets you modify the view’s size and location using the view’s frame, bounds, or center properties, allowing you to create a static, frame-based layout within Auto Layout.
Note that the autoresizing mask constraints fully specify the view’s size and position; therefore, you cannot add additional constraints to modify this size or position without introducing conflicts. If you want to use Auto Layout to dynamically calculate the size and position of your view, you must set this property to false, and then provide a non ambiguous, nonconflicting set of constraints for the view.
By default, the property is set to true for any view you programmatically create. If you add views in Interface Builder, the system automatically sets this property to false.
对于那些感兴趣的人,下面是我的自定义 UIViewControllerTransitioningDelegate
的最终代码。仅使用自动布局约束,并且仅修改上述视图属性。
注意:我正在使用TinyConstraints来使编写约束更加愉快。
//
// FullScreenTransitionManager.swift
//
import Foundation
import UIKit
import TinyConstraints
// MARK: FullScreenPresentationController
final class FullScreenPresentationController: UIPresentationController {
private lazy var backgroundView: UIVisualEffectView = {
let blurVisualEffectView = UIVisualEffectView(effect: blurEffect)
blurVisualEffectView.effect = nil
return blurVisualEffectView
}()
private let blurEffect = UIBlurEffect(style: .systemThinMaterial)
private lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap))
@objc private func onTap(_ gesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true)
}
override func presentationTransitionWillBegin() {
guard let containerView = containerView else { return }
containerView.addGestureRecognizer(tapGestureRecognizer)
containerView.addSubview(backgroundView)
backgroundView.edgesToSuperview()
guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
transitionCoordinator.animate(alongsideTransition: { context in
self.backgroundView.effect = self.blurEffect
})
}
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.effect = nil
})
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
if completed {
backgroundView.removeFromSuperview()
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? {
guard let anchorView = anchorView else { return nil }
return FullScreenAnimationController(animationType: .present, anchorView: anchorView)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let anchorView = anchorView else { return nil }
return FullScreenAnimationController(animationType: .dismiss, anchorView: anchorView)
}
}
// MARK: UIViewControllerAnimatedTransitioning
final class FullScreenAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
fileprivate enum AnimationType {
case present
case dismiss
}
private let animationType: AnimationType
private let anchorViewCenter: CGPoint
private let anchorViewSize: CGSize
private let anchorViewTag: Int
private let animationDuration: TimeInterval
private var propertyAnimator: UIViewPropertyAnimator?
fileprivate init(animationType: AnimationType, anchorView: UIView, animationDuration: TimeInterval = 0.3) {
self.animationType = animationType
self.anchorViewCenter = anchorView.superview?.convert(anchorView.center, to: nil) ?? .zero
self.anchorViewSize = anchorView.bounds.size
self.anchorViewTag = anchorView.tag
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)
toViewController.view.edgesToSuperview()
toViewController.view.layoutIfNeeded() // Force a layout update so that the view is ready for the animator
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 view: UIView = viewController.view.viewWithTag(anchorViewTag) ?? viewController.view
let finalSize = view.bounds.size
let finalCenter = view.center
view.transform = CGAffineTransform(scaleX: anchorViewSize.width / finalSize.width,
y: anchorViewSize.height / finalSize.height)
view.center = view.superview!.convert(anchorViewCenter, from: nil)
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut], animations: {
view.transform = .identity
view.center = finalCenter
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
private func dismissAnimator(with transitionContext: UIViewControllerContextTransitioning,
animating viewController: UIViewController) -> UIViewPropertyAnimator {
let view: UIView = viewController.view.viewWithTag(anchorViewTag) ?? viewController.view
let initialSize = view.bounds.size
let finalCenter = view.superview!.convert(anchorViewCenter, from: nil)
let finalTransform = CGAffineTransform(scaleX: self.anchorViewSize.width / initialSize.width,
y: self.anchorViewSize.height / initialSize.height)
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut], animations: {
view.transform = finalTransform
view.center = finalCenter
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
那么,开门见山:
我正在使用自定义的 UIViewControllerTransitioningDelegate,它提供自定义的 UIPresentationController 和 present/dismiss 动画来为视图设置动画从一个视图控制器到另一个。当在第一个视图控制器的 table 视图单元格中粘贴图像时,图像在第二个视图控制器中以全屏显示,从它在 table 视图单元格中的位置到它在呈现的视图控制器。
下面的 gif 显示了正在发生的事情。请注意,当前动画一切正常,但关闭动画则不然。
我遇到的问题是,当关闭动画触发时,看起来动画视图的框架在某种程度上发生了偏移或变形。我不知道为什么!动画开始时的帧未被触及(至少我是这样),动画结束时的帧与当前动画的帧相同 - 效果非常好!
有人知道发生了什么事吗?
下面提供了我的自定义 UIViewControllerTransitioningDelegate 的代码。
//
// FullScreenTransitionManager.swift
//
import Foundation
import UIKit
// MARK: FullScreenPresentationController
final class FullScreenPresentationController: UIPresentationController {
private lazy var backgroundView: UIVisualEffectView = {
let blurVisualEffectView = UIVisualEffectView(effect: blurEffect)
blurVisualEffectView.effect = nil
return blurVisualEffectView
}()
private let blurEffect = UIBlurEffect(style: .systemThinMaterial)
private lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap))
@objc private func onTap(_ gesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true)
}
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.effect = self.blurEffect
})
}
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.effect = nil
})
}
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? {
guard let anchorView = anchorView, let anchorViewSuperview = anchorView.superview else { return nil }
let anchorViewFrame = CGRect(origin: anchorViewSuperview.convert(anchorView.frame.origin, to: nil), size: anchorView.frame.size)
let anchorViewTag = anchorView.tag
return FullScreenAnimationController(animationType: .present, anchorViewFrame: anchorViewFrame, anchorViewTag: anchorViewTag)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let anchorView = anchorView, let anchorViewSuperview = anchorView.superview else { return nil }
let anchorViewFrame = CGRect(origin: anchorViewSuperview.convert(anchorView.frame.origin, to: nil), size: anchorView.frame.size)
let anchorViewTag = anchorView.tag
return FullScreenAnimationController(animationType: .dismiss, anchorViewFrame: anchorViewFrame, anchorViewTag: anchorViewTag)
}
}
// MARK: UIViewControllerAnimatedTransitioning
final class FullScreenAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum AnimationType {
case present
case dismiss
}
private let animationType: AnimationType
private let anchorViewFrame: CGRect
private let anchorViewTag: Int
private let animationDuration: TimeInterval
private var propertyAnimator: UIViewPropertyAnimator?
init(animationType: AnimationType, anchorViewFrame: CGRect, anchorViewTag: Int, animationDuration: TimeInterval = 0.3) {
self.animationType = animationType
self.anchorViewFrame = anchorViewFrame
self.anchorViewTag = anchorViewTag
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 window = transitionContext.containerView.window!
let finalRootViewFrame = transitionContext.finalFrame(for: viewController)
viewController.view.frame = finalRootViewFrame
viewController.view.setNeedsUpdateConstraints()
viewController.view.setNeedsLayout()
viewController.view.layoutIfNeeded()
let view: UIView = viewController.view.viewWithTag(anchorViewTag) ?? viewController.view
let finalFrame = view.frame
view.frame = CGRect(origin: window.convert(anchorViewFrame.origin, to: view.superview!), size: anchorViewFrame.size)
view.setNeedsUpdateConstraints()
view.setNeedsLayout()
view.layoutIfNeeded()
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut], animations: {
view.frame = finalFrame
view.setNeedsUpdateConstraints()
view.setNeedsLayout()
view.layoutIfNeeded()
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
private func dismissAnimator(with transitionContext: UIViewControllerContextTransitioning,
animating viewController: UIViewController) -> UIViewPropertyAnimator {
let window = transitionContext.containerView.window!
let view: UIView = viewController.view.viewWithTag(anchorViewTag) ?? viewController.view
let finalFrame = CGRect(origin: window.convert(anchorViewFrame.origin, to: view.superview!), size: anchorViewFrame.size)
viewController.view.setNeedsUpdateConstraints()
viewController.view.setNeedsLayout()
viewController.view.layoutIfNeeded()
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut], animations: {
view.frame = finalFrame
view.setNeedsUpdateConstraints()
view.setNeedsLayout()
view.layoutIfNeeded()
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
同时为我的 FullScreenImageViewController 添加代码。
//
// FullScreenImageViewController.swift
//
import UIKit
import TinyConstraints
class FullScreenImageViewController: UIViewController {
private let imageView: UIImageView = {
let image = UIImage(named: "Bananas")!
let imageView = UIImageView(image: image)
let aspectRatio = imageView.intrinsicContentSize.width / imageView.intrinsicContentSize.height
imageView.contentMode = .scaleAspectFit
imageView.widthToHeight(of: imageView, multiplier: aspectRatio)
return imageView
}()
private lazy var imageViewWidthConstraint = imageView.widthToSuperview(relation: .equalOrLess)
private lazy var imageViewHeightConstraint = imageView.heightToSuperview(relation: .equalOrLess)
init(tag: Int) {
super.init(nibName: nil, bundle: nil)
imageView.tag = tag
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
traitCollectionChanged(from: previousTraitCollection)
}
private func configureUI() {
view.backgroundColor = .clear
view.addSubview(imageView)
imageView.centerInSuperview()
traitCollectionChanged(from: nil)
}
private func traitCollectionChanged(from previousTraitCollection: UITraitCollection?) {
if traitCollection.horizontalSizeClass != .compact {
// Landscape
imageViewWidthConstraint.isActive = false
imageViewHeightConstraint.isActive = true
} else {
// Portrait
imageViewWidthConstraint.isActive = true
imageViewHeightConstraint.isActive = false
}
}
}
以及实际呈现 FullScreenImageViewController 的代码(只是为了更好的衡量)
//
// ViewController.swift
//
import UIKit
class ViewController: UITableViewController {
// ...
// ...
private var fullScreenTransitionManager: FullScreenTransitionManager?
// ...
// ...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? TableViewCell else { return }
let fullScreenTransitionManager = FullScreenTransitionManager(anchorView: cell.bananaImageView)
let viewController = FullScreenImageViewController(tag: cell.bananaImageView.tag)
viewController.modalPresentationStyle = .custom
viewController.transitioningDelegate = fullScreenTransitionManager
present(viewController, animated: true)
self.fullScreenTransitionManager = fullScreenTransitionManager
}
}
玩了半天终于搞明白了。 而且我想我一直对问题的根源有一种感觉...
简答:
使用自动布局约束时,不要尝试通过更改 .frame
或 .bounds
来为视图设置动画。更改这些属性可能会导致未定义的行为(就像我遇到的那样)。相反,通过更改约束或 .center
and/or .transform
属性 来动画视图。这些属性不与布局引擎冲突。查询视图的大小时,请使用 .bounds
属性,因为在使用自动布局约束时此 属性 比 .frame
更可靠。
稍微长一点的回答:
由于我到处都在使用自动布局约束,因此将它与在动画期间手动更改视图帧相结合是行不通的。或者更正确 - 有未定义的行为。由于自动布局引擎使用约束为您修改视图的框架,因此您应该避免自己触摸 .frame
(和 .bounds
)属性。相反,通过更改 .center
和 .transform
等属性来为您的视图添加动画效果。这些属性似乎与自动布局不冲突,并且在自动布局引擎完成计算后,对这些属性的更改将应用于您的视图。事件想法更改视图的 .frame
和 .bounds
有时可能会与自动布局约束结合使用,就像我体验自定义演示动画一样(它似乎完美无缺!),你真的应该避免它.在某些情况下,一个解决方法可能是临时转向 .translatesAutoresizingMaskIntoConstraints == true
,但这真的不是一个好主意,因为它会导致 UIKit 为您生成自动布局约束,而这些约束可能会与您自己的约束冲突。查询视图的大小时,请使用 .bounds
属性,因为在使用自动布局约束和 .transform
[时,此 属性 比 .frame
更可靠=63=].
Apple 文档中值得一提的地方:
UIView.center
:
Use this property, instead of the frame property, when you want to change the position of a view. The center point is always valid, even when scaling or rotation factors are applied to the view's transform. Changes to this property can be animated.
UIView.transform
:
In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame.
Warning: When the value of this property is anything other than the identity transform, the value in the frame property is undefined and should be ignored.
UIView.translatesAutoresizingMaskIntoConstraints
:
If this property’s value is true, the system creates a set of constraints that duplicate the behavior specified by the view’s autoresizing mask. This also lets you modify the view’s size and location using the view’s frame, bounds, or center properties, allowing you to create a static, frame-based layout within Auto Layout.
Note that the autoresizing mask constraints fully specify the view’s size and position; therefore, you cannot add additional constraints to modify this size or position without introducing conflicts. If you want to use Auto Layout to dynamically calculate the size and position of your view, you must set this property to false, and then provide a non ambiguous, nonconflicting set of constraints for the view.
By default, the property is set to true for any view you programmatically create. If you add views in Interface Builder, the system automatically sets this property to false.
对于那些感兴趣的人,下面是我的自定义 UIViewControllerTransitioningDelegate
的最终代码。仅使用自动布局约束,并且仅修改上述视图属性。
注意:我正在使用TinyConstraints来使编写约束更加愉快。
//
// FullScreenTransitionManager.swift
//
import Foundation
import UIKit
import TinyConstraints
// MARK: FullScreenPresentationController
final class FullScreenPresentationController: UIPresentationController {
private lazy var backgroundView: UIVisualEffectView = {
let blurVisualEffectView = UIVisualEffectView(effect: blurEffect)
blurVisualEffectView.effect = nil
return blurVisualEffectView
}()
private let blurEffect = UIBlurEffect(style: .systemThinMaterial)
private lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap))
@objc private func onTap(_ gesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true)
}
override func presentationTransitionWillBegin() {
guard let containerView = containerView else { return }
containerView.addGestureRecognizer(tapGestureRecognizer)
containerView.addSubview(backgroundView)
backgroundView.edgesToSuperview()
guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
transitionCoordinator.animate(alongsideTransition: { context in
self.backgroundView.effect = self.blurEffect
})
}
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.effect = nil
})
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
if completed {
backgroundView.removeFromSuperview()
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? {
guard let anchorView = anchorView else { return nil }
return FullScreenAnimationController(animationType: .present, anchorView: anchorView)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let anchorView = anchorView else { return nil }
return FullScreenAnimationController(animationType: .dismiss, anchorView: anchorView)
}
}
// MARK: UIViewControllerAnimatedTransitioning
final class FullScreenAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
fileprivate enum AnimationType {
case present
case dismiss
}
private let animationType: AnimationType
private let anchorViewCenter: CGPoint
private let anchorViewSize: CGSize
private let anchorViewTag: Int
private let animationDuration: TimeInterval
private var propertyAnimator: UIViewPropertyAnimator?
fileprivate init(animationType: AnimationType, anchorView: UIView, animationDuration: TimeInterval = 0.3) {
self.animationType = animationType
self.anchorViewCenter = anchorView.superview?.convert(anchorView.center, to: nil) ?? .zero
self.anchorViewSize = anchorView.bounds.size
self.anchorViewTag = anchorView.tag
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)
toViewController.view.edgesToSuperview()
toViewController.view.layoutIfNeeded() // Force a layout update so that the view is ready for the animator
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 view: UIView = viewController.view.viewWithTag(anchorViewTag) ?? viewController.view
let finalSize = view.bounds.size
let finalCenter = view.center
view.transform = CGAffineTransform(scaleX: anchorViewSize.width / finalSize.width,
y: anchorViewSize.height / finalSize.height)
view.center = view.superview!.convert(anchorViewCenter, from: nil)
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut], animations: {
view.transform = .identity
view.center = finalCenter
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
private func dismissAnimator(with transitionContext: UIViewControllerContextTransitioning,
animating viewController: UIViewController) -> UIViewPropertyAnimator {
let view: UIView = viewController.view.viewWithTag(anchorViewTag) ?? viewController.view
let initialSize = view.bounds.size
let finalCenter = view.superview!.convert(anchorViewCenter, from: nil)
let finalTransform = CGAffineTransform(scaleX: self.anchorViewSize.width / initialSize.width,
y: self.anchorViewSize.height / initialSize.height)
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut], animations: {
view.transform = finalTransform
view.center = finalCenter
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}