iOS 13款不依赖快照的UIPresentationController?

iOS 13 style UIPresentationController without relying on snapshots?

iOS 13 似乎使用新的 UIPresentationController 来呈现模态视图控制器,但它不依赖于拍摄呈现视图控制器的快照(因为大多数/所有库都这样做) ).呈现视图控制器 'live' 并在模态视图控制器显示在透明/有色背景上方时继续显示动画/更改。

我可以很容易地复制这个(因为目的是为 iOS 10 / 11 / 12 等制作一个向后兼容的版本)通过在呈现视图控制器上使用 CGAffineTransform视图,但是在旋转设备时,无论多么频繁,呈现的视图都开始变形并超出范围,这似乎是因为系统更新了它的 frame,同时应用了一个活动的 transform

根据文档,当 transform 应用于视图时,frame 未定义。鉴于系统似乎正在修改框架而不是我,我该如何解决这个问题而不会在我更新呈现视图的边界时得到 hacky 解决方案?我需要这个呈现控制器保持通用,因为呈现控制器可以是任何形状或形式,不一定是全屏视图。

这是我目前所拥有的 - 这是一个简单的 UIPresentationController 子类,它似乎工作正常,但是旋转设备然后关闭呈现的视图控制器似乎会改变呈现视图控制器的边界(变得太宽或缩小,具体取决于您是在横向/纵向模式下呈现模态控制器)

class SheetPresentationController: UIPresentationController {
  override var frameOfPresentedViewInContainerView: CGRect {
    return CGRect(x: 40, y: containerView!.bounds.height / 2, width: containerView!.bounds.width-80, height: containerView!.bounds.height / 2)
  }

  override func containerViewWillLayoutSubviews() {
    super.containerViewWillLayoutSubviews()

    if let _ = presentingViewController.transitionCoordinator {
      // We're transitioning - don't touch the frame yet as it'll
      // clash with our transform
    } else {
      self.presentedView?.frame = self.frameOfPresentedViewInContainerView
    }
  }

  override func presentationTransitionWillBegin() {
    super.presentationTransitionWillBegin()

    containerView?.backgroundColor = .clear

    if let coordinator = presentingViewController.transitionCoordinator {
      coordinator.animate(alongsideTransition: { [weak self] _ in
        self?.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.3)

        // Scale the presenting view
        self?.presentingViewController.view.layer.cornerRadius = 16

        self?.presentingViewController.view.transform = CGAffineTransform.init(scaleX: 0.9, y: 0.9)
        }, completion: nil)
    }
  }

  override func dismissalTransitionWillBegin() {
    if let coordinator = presentingViewController.transitionCoordinator {
      coordinator.animate(alongsideTransition: { [weak self] _ in
        self?.containerView?.backgroundColor = .clear

        self?.presentingViewController.view.layer.cornerRadius = 0
        self?.presentingViewController.view.transform = .identity
        }, completion: nil)
    }
  }
}

以及呈现动画控制器:

import UIKit

final class PresentingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    guard let presentedViewController = transitionContext.viewController(forKey: .to) else {
      return
    }

    let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
    let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)

    let containerView = transitionContext.containerView
    containerView.addSubview(presentedViewController.view)

    let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)
    presentedViewController.view.frame = finalFrameForPresentedView

    // Move it below the screen so it slides up
    presentedViewController.view.frame.origin.y = containerView.bounds.height

    animator.addAnimations {
      presentedViewController.view.frame = finalFrameForPresentedView      
    }

    animator.addCompletion { (animationPosition) in
      if animationPosition == .end {
        transitionContext.completeTransition(true)
      }
    }

    animator.startAnimation()
  }

  func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.6
  }
}

以及解雇动画控制器:

import UIKit

final class DismissingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    guard let presentedViewController = transitionContext.viewController(forKey: .from) else {
      return
    }

    guard let presentingViewController = transitionContext.viewController(forKey: .to) else {
      return
    }

    let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)

    let containerView = transitionContext.containerView
    let offscreenFrame = CGRect(x: finalFrameForPresentedView.minX, y: containerView.bounds.height, width: finalFrameForPresentedView.width, height: finalFrameForPresentedView.height)

    let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
    let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)

    animator.addAnimations {
      presentedViewController.view.frame = offscreenFrame
    }

    animator.addCompletion { (position) in
      if position == .end {
        // Complete transition        
        transitionContext.completeTransition(true)
      }
    }

    animator.startAnimation()
  }

  func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.6
  }
}

自定义演示文稿是 UIKit 的一个棘手部分。这是我想到的,没有保证 ;-)

我建议您尝试 "commit" 呈现视图上的动画 - 因此在 presentationTransitionDidEnd(Bool) 回调中删除转换并在呈现视图上设置与转换所做的相匹配的适当约束。或者您也可以只为约束更改设置动画以模仿变换。

如果发生轮换,您可能会收到 viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 回电以管理正在进行的演示。

好的,我明白了。似乎 iOS 13 没有使用比例变换。正如所解释的那样,在你这样做的那一刻,旋转设备将修改呈现视图的框架,并且由于你已经将 transform 应用于视图,视图将以意想不到的方式调整大小并且转换不会不再有效。

解决方案是改用 z-axis 视角,这会给你完全相同的结果,但这样做会在旋转等情况下幸存下来,因为你所做的只是将视图移回 3D space (Z-axis),从而有效地缩小它。这是为我做的转换 (Swift):

  func calculatePerspectiveTransform() -> CATransform3D {
    let eyePosition:Float = 10.0;
    var contentTransform:CATransform3D = CATransform3DIdentity
    contentTransform.m34 = CGFloat(-1/eyePosition)
    contentTransform = CATransform3DTranslate(contentTransform, 0, 0, -2)
    return contentTransform
  }

这是一篇解释其工作原理的文章:https://whackylabs.com/uikit/2014/10/29/add-some-perspective-to-your-uiviews/

在您的 UIPresenterController 中,您还需要执行以下操作才能正确处理这种跨旋转变换:

  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    // Reset transform before we rotate and then apply it again during rotation
    if let presentingView = presentingViewController.view {
      presentingView.layer.transform = CATransform3DIdentity
    }

    coordinator.animate(alongsideTransition: { [weak self] (context) in
      if let presentingView = self?.presentingViewController.view {
        presentingView.layer.transform = self?.calculatePerspectiveTransform() ?? CATransform3DIdentity
      }
    })
  }