【问题标题】:iOS 13 style UIPresentationController without relying on snapshots?iOS 13 风格的 UIPresentationController 不依赖快照?
【发布时间】:2019-11-24 16:37:26
【问题描述】:

iOS 13 似乎使用新的UIPresentationController 来呈现模态视图控制器,但它不依赖于拍摄呈现视图控制器的快照(正如大多数/所有库所做的那样)。呈现视图控制器处于“实时”状态,并在模态视图控制器显示在透明/着色背景上方时继续显示动画/更改。

我可以通过在呈现视图控制器的视图上使用CGAffineTransform 轻松复制这一点(因为目标是为 iOS 10 / 11 / 12 等制作向后兼容的版本),但在旋转设备时频繁,呈现视图开始变形并超出界限,似乎是因为系统更新了它的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)
    }
  }
}

还有 Presenting Animation 控制器:

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
  }
}

【问题讨论】:

    标签: ios swift xcode uipresentationcontroller


    【解决方案1】:

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

    解决方案是改用 z 轴透视图,这将为您提供完全相同的结果,但这样做会在旋转等情况下存活,因为您所做的只是将视图移回 3D 空间(Z 轴) ,从而有效地缩小它。这是为我完成此操作的转换 (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
          }
        })
      }
    

    【讨论】:

      【解决方案2】:

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

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

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

      【讨论】:

      • 是的,这些是我试图避免的“hacky”解决方案。任何约束都无法模仿使用CALayer / View 变换获得的漂亮缩放,因此这是不可行的。我也不想弄乱演示视图的框架,因为演示者不拥有它,也不应该假设演示视图采用什么形状/大小/形式。
      • 公平地说,iOS 13 做得很好,所以我确信有一个更干净的方法可以解决这个问题。也许他们正在将呈现视图移动到他们拥有和缩放的中间视图,然后在完成后将其移回。很难说。
      猜你喜欢
      • 2021-02-16
      • 2017-02-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-28
      • 1970-01-01
      • 1970-01-01
      • 2011-04-03
      相关资源
      最近更新 更多