【问题标题】:View’s frame jumps right before dismiss animation, using custom UIViewControllerTransitioningDelegate使用自定义 UIViewControllerTransitioningDelegate 在关闭动画之前视图的帧跳转
【发布时间】:2022-01-19 17:26:45
【问题描述】:

那么,直截了当:

我正在使用自定义 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
    }
}

【问题讨论】:

    标签: ios swift animation autolayout


    【解决方案1】:

    玩了一阵子后,我设法弄明白了。 而且我想我一直对问题所在有一种感觉......

    简答: 使用自动布局约束时,不要尝试通过更改其 .frame.bounds 来为视图设置动画。更改这些属性可能会导致未定义的行为(就像我遇到的那样)。相反,通过更改其约束或.center 和/或.transform 属性来为视图设置动画。这些属性与布局引擎不冲突。在查询视图的大小时,请使用.bounds 属性,因为在使用自动布局约束时,此属性比.frame 更可靠。

    答案稍长: 由于我到处都在使用自动布局约束,因此将其与在动画期间手动更改视图框架相结合是行不通的。或者更正确 - 有未定义的行为。由于自动布局引擎使用约束来为您修改视图的框架,因此您应该避免自己接触.frame(和.bounds)属性。相反,您可以通过更改 .center.transform 等属性来为您的视图设置动画。这些属性似乎与 Auto Layout 不冲突,并且在 Auto Layout Engine 完成计算后,对这些属性的更改将应用​​于您的视图。事件认为更改视图的.frame.bounds 有时可能与自动布局约束结合使用,就像我在自定义演示动画中所经历的那样(这似乎完美无缺!),你应该真正避免它。在某些情况下,一种解决方法可能是临时转向.translatesAutoresizingMaskIntoConstraints == true,但这确实不是一个好主意,因为它会导致 UIKit 为您生成自动布局约束,并且这些约束可能与您自己的约束冲突。在查询视图的大小时,请使用.bounds 属性,因为在使用自动布局约束和.transform 属性时,此属性比.frame 更可靠。

    Apple 文档中值得提及的内容:

    UIView.center:

    当您想要更改视图的位置时,请使用此属性而不是 frame 属性。中心点始终有效,即使将缩放或旋转因子应用于视图的变换也是如此。可以动画对此属性的更改。

    UIView.transform:

    在 iOS 8.0 及更高版本中,transform 属性不会影响自动布局。自动布局根据未转换的框架计算视图的对齐矩形。

    警告: 当此属性的值不是恒等变换时,frame 属性中的值是未定义的,应该被忽略。

    UIView.translatesAutoresizingMaskIntoConstraints:

    如果此属性的值为 true,系统会创建一组约束,这些约束复制视图的自动调整掩码指定的行为。这还允许您使用视图的框架、边界或中心属性修改视图的大小和位置,从而允许您在自动布局中创建基于框架的静态布局。

    请注意,自动调整掩码约束完全指定了视图的大小和位置;因此,您不能添加额外的约束来修改此大小或位置而不引入冲突。如果要使用 Auto Layout 动态计算视图的大小和位置,则必须将此属性设置为 false,然后为视图提供一组明确、不冲突的约束。

    默认情况下,对于您以编程方式创建的任何视图,该属性都设置为 true。如果在 Interface Builder 中添加视图,系统会自动将此属性设置为 false。

    对于那些感兴趣的人,下面是我自定义UIViewControllerTransitioningDelegate 的最终代码。仅使用 Auto Layout Constraints,并且仅修改上述视图属性。 注意:我使用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)
            })
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2011-05-08
      • 1970-01-01
      • 2013-11-27
      • 2013-04-15
      • 1970-01-01
      • 1970-01-01
      • 2015-12-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多