那么,直接進入正題:
我正在使用自定義UIViewControllerTransitioningDelegate,它提供自定義UIPresentationController和呈現/關閉影片,將視圖從一個視圖控制器影片到另一個視圖控制器。當影像在第一個視圖控制器的表格視圖單元中被粘貼時,影像在第二個視圖控制器中以全屏顯示,從它在表格視圖單元中的位置影片到它在呈現的視圖控制器中的位置。
下面的 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
}
}
uj5u.com熱心網友回復:
在玩了一段時間后,我設法弄清楚了。而且我想我一直對問題所在有一種感覺......
簡短回答:
不要嘗試通過更改視圖.frame或.bounds使用自動布局約束來為視圖設定影片。更改這些屬性可能會導致未定義的行為(就像我經歷的那樣)。相反,通過更改其約束或.center和/或.transform屬性來為視圖設定影片。這些屬性與布局引擎不沖突。當查詢視圖的大小時,請使用該.bounds屬性,因為該屬性比.frame使用自動布局約束時更可靠。
稍微長一點的答案:
由于我到處都在使用自動布局約束,因此在影片期間將其與手動更改視圖幀相結合是行不通的。或者更正確 - 有未定義的行為。由于自動布局引擎使用約束為您修改視圖的框架,因此您應該避免自己接觸.frame(and .bounds) 屬性。相反,通過更改.center和 等屬性來為您的視圖設定影片.transform。這些屬性似乎與 Auto Layout 沒有沖突,在 Auto Layout Engine 完成計算后,對這些屬性的更改將應用??于您的視圖。事件思想改變.frame和.bounds有時可能會與自動布局約束結合使用,就像我在自定義演示影片中體驗到的一樣(似乎完美無缺!),你真的應該避免它。某些情況下的解決方法可能是臨時 turn .translatesAutoresizingMaskIntoConstraints == true,但這確實不是一個好主意,因為它會導致 UIKit 為您生成自動布局約束,而這些約束可能與您自己的約束沖突。查詢視圖的大小時,請使用該.bounds屬性,因為該屬性比.frame使用自動布局約束和該.transform屬性時更可靠。
Apple 檔案中值得一提的地方:
UIView.center:
當您想要更改視圖的位置時,請使用此屬性而不是 frame 屬性。中心點始終有效,即使將縮放或旋轉因子應用于視圖的變換也是如此。可以影片化對此屬性的更改。
UIView.transform:
在 iOS 8.0 及更高版本中,transform 屬性不會影響自動布局。自動布局根據其未轉換的框架計算視圖的對齊矩形。
警告: 當該屬性的值不是恒等變換時,frame 屬性中的值是未定義的,應該被忽略。
UIView.translatesAutoresizingMaskIntoConstraints:
如果此屬性的值為 true,則系統會創建一組約束來復制由視圖的自動調整大小掩碼指定的行為。這還允許您使用視圖的框架、邊界或中心屬性修改視圖的大小和位置,從而允許您在自動布局中創建靜態的、基于框架的布局。
請注意,自動調整大小蒙版約束完全指定了視圖的大小和位置;因此,您不能在不引入沖突的情況下添加額外的約束來修改此大小或位置。如果要使用自動布局來動態計算視圖的大小和位置,則必須將此屬性設定為 false,然后為視圖提供一組明確、無沖突的約束。
默認情況下,對于您以編程方式創建的任何視圖,該屬性都設定為 true。如果在 Interface Builder 中添加視圖,系統會自動將此屬性設定為 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)
})
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/388673.html
上一篇:矩形內的Qml影片不起作用
下一篇:使用自定義UIViewControllerTransitioningDelegate在視圖控制器關閉期間不考慮additionalSafeAreaInsets
