UINavigationController 忘记如何在自定义过渡动画后旋转子控制器

UINavigationController forgets how to rotate sub controllers after custom transition animation

几天来我一直在与以下问题作斗争。我在文档中找不到任何关于我做错了什么的信息。我已浏览调试器并将各种信息转储到控制台,但无法解决我所看到的问题。

我的顶级视图控制器是 UINavigationController 的子class。这包含 UIPageViewController 的子class,它将用于可视化我的数据。它还包含 UITableViewController 的子class,用于设置应用程序设置。这个 "options" 视图是 "linked" 通过推送 segue 到页面视图控制器的。此时,一切正常。

当我想区分数据可视化的各种方式(页面视图控制器中的水平滚动)和引入选项视图之间的视觉转换时,问题就出现了。因此,我创建了一个自定义动画 "stretch in" 选项控制器从屏幕顶部(推送时)或 "roll up" 选项控制器到屏幕顶部(弹出时)。我使用 navigationController:animationControllerFor:operation:from:to: 方法在导航控制器上设置此动画。

在显示选项控制器之前,设备旋转期间一切似乎都正常。 displaying/dismissing选项控制器之后,数据可视化控制器的布局在旋转过程中发生故障。当关闭选项控制器时,它以设备所在的方向正确显示,但在相反的方向上播放不正确。

导航控制器(或者可能是页面视图控制器)似乎忘记了如何处理状态、导航和工具栏的高度。


下面是5s模拟器上的调试截图说明问题

  1. 初始状态。请注意,内容视图(红色)在状态、导航和工具栏下方

  1. 已将方向旋转为水平。到目前为止一切正常。调试控制台显示视图的大小 "rotated" 到 (568x212),状态、导航和工具栏的 before/after 高度、屏幕大小、框架矩形和图层位置。我没有在这里展示它,但是图层锚点从 (0.5, 0.5) 开始从未改变。

请注意,目标旋转大小是使用以下规则设置的:

生成的帧大小使用以下规则设置:

并且生成的框架原点 (0,64) 从屏幕顶部偏移了状态栏 (20) 和导航栏 (44) 的总和。

  1. 已将方向旋转回垂直方向。同样,一切正常。调试控制台为 "reverse" 轮换添加了与上面相同的信息。旋转大小和生成的帧大小遵循与上述相同的规则。

  1. 使用自定义动画将选项编辑器控制器视图推送到导航堆栈。调试控制台为(当前不可见的)内容视图添加与上面相同的信息。请注意,没有任何变化。

  1. 使用自定义动画将选项编辑器控制器视图从导航堆栈中弹出。上面的内容查看returns查看。调试控制台添加与上面相同的信息。同样,一切都没有改变。

注意,堆叠顺序与选项编辑器可见之前不同

当使用默认的过渡动画器(通过从 pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted 返回 nil)时,此堆栈重新排序不成立

  1. 重复第 1 步。这就是“不稳定”的发展方向。内容视图不再紧贴导航和工具栏。调试控制台显示 进入 旋转的所有内容看起来都与步骤 1 中的相同。

BUT,旋转的结果和步骤1不一样,规则好像变了。生成的框架大小 不再 调整以考虑状态、导航和工具栏高度的变化,并且框架原点与旋转前的原点相比没有变化。

  1. 重复第 2 步。似乎一切都已修复,但这只是因为它正在按照新的调整大小规则播放。


我的动画师 class 在此处显示(完整的,除了用于生成上面显示的调试信息的探针):

class OptionsViewAnimator: NSObject, UIViewControllerAnimatedTransitioning
{
  var type : UINavigationControllerOperation

  init(_ type : UINavigationControllerOperation)
  {
    self.type = type
  }

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

  func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
  {
    if      self.type == .push { showOptions(using:transitionContext) }
    else if self.type == .pop  { hideOptions(using:transitionContext) }
  }

  func showOptions(using context: UIViewControllerContextTransitioning)
  {
    let src       = context.viewController(forKey: .from)!
    let srcView   = context.view(forKey: .from)!
    let dstView   = context.view(forKey: .to)!
    var dstEnd    = context.finalFrame(for: context.viewController(forKey: .to)!)

    let barHeight = (src.navigationController?.toolbar.frame.height) ?? 0.0

    dstEnd.size.height += barHeight

    dstView.frame = dstEnd
    dstView.layer.position = dstEnd.origin
    dstView.layer.anchorPoint = CGPoint(x:0.0,y:0.0)
    dstView.transform = dstView.transform.scaledBy(x: 1.0, y: 0.01)

    UIApplication.shared.keyWindow!.insertSubview(dstView, aboveSubview: srcView)

    UIView.animate(withDuration: 0.35, animations:
      {
        dstView.transform = .identity
      }
    ) {
      (finished)->Void in
      context.completeTransition( !context.transitionWasCancelled )
    }
  }

  func hideOptions(using context: UIViewControllerContextTransitioning)
  {
    let dst       = context.viewController(forKey: .to)!
    let srcView   = context.view(forKey: .from)!
    let dstView   = context.view(forKey: .to)!
    let dstEnd    = context.finalFrame(for: context.viewController(forKey: .to)!)

    dstView.frame = dstEnd

    srcView.layer.position = dstEnd.origin
    srcView.layer.anchorPoint = CGPoint(x:0.0,y:0.0)
    srcView.transform = .identity

    UIApplication.shared.keyWindow!.insertSubview(dstView, belowSubview: srcView)

    UIView.animate(withDuration: 0.35, animations:
      {
        srcView.transform = srcView.transform.scaledBy(x: 1.0, y: 0.01)
      }
    ) {
      (finished)->Void in
      context.completeTransition( !context.transitionWasCancelled )
    }
  }
}

感谢 any/all 帮助。 麦克

既然你想让它覆盖 UINavigation Controller Bar,那么有什么理由不使用自定义模态(模态地出现在故事​​板中)。无论哪种方式,我构建它都是为了避免添加到 window。测试后告诉我,但在我的测试中,它可以完美地与任何一个一起使用。

 import UIKit

class OptionsViewAnimator: NSObject, UIViewControllerAnimatedTransitioning,UIViewControllerTransitioningDelegate
{
  var isPresenting = true
  fileprivate var isNavPushPop = false

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

  func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
  {
    if isPresenting { showOptions(using:transitionContext) }
    else            { hideOptions(using:transitionContext) }
  }

  func showOptions(using context: UIViewControllerContextTransitioning)
  {
    let src       = context.viewController(forKey: .from)!
    let dstView   = context.view(forKey: .to)!
    let frame = context.finalFrame(for: context.viewController(forKey: .to)!)
    let container = context.containerView
    dstView.frame = frame
    dstView.layer.position = CGPoint(x: container.frame.origin.x, y: container.frame.origin.y + frame.origin.y)
    print("container = \(container.frame)")
    dstView.layer.anchorPoint = CGPoint(x:container.frame.origin.x,y:container.frame.origin.y)
    dstView.transform = dstView.transform.scaledBy(x: 1.0, y: 0.01)
    container.addSubview(dstView)

    UIView.animate(withDuration: 0.35, animations: { dstView.transform = .identity } )
    {
      (finished)->Void in
        src.view.transform = .identity
      context.completeTransition( !context.transitionWasCancelled )
    }
  }

  func hideOptions(using context: UIViewControllerContextTransitioning)
  {
    let srcView   = context.view(forKey: .from)!
    let dstView   = context.view(forKey: .to)!

    let container = context.containerView
    container.insertSubview(dstView, belowSubview: srcView)
    srcView.layer.anchorPoint = CGPoint(x:container.frame.origin.x,y:container.frame.origin.y)
    dstView.frame = context.finalFrame(for: context.viewController(forKey: .to)!)
    dstView.layoutIfNeeded()
    UIView.animate(withDuration: 0.35, animations:
      { srcView.transform = srcView.transform.scaledBy(x: 1.0, y: 0.01) } )
    {
      (finished)->Void in

      context.completeTransition( !context.transitionWasCancelled )
    }
  }

  func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
  {
    isPresenting = false
    return self
  }


  func animationController(forPresented presented: UIViewController,
                           presenting: UIViewController,
                           source: UIViewController) -> UIViewControllerAnimatedTransitioning?
  {
    isPresenting = true
    return self
  }
}

extension OptionsViewAnimator: UINavigationControllerDelegate
{
  func navigationController(_ navigationController: UINavigationController,
                            animationControllerFor operation: UINavigationControllerOperation,
                            from fromVC: UIViewController,
                            to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
  {
    isNavPushPop = true
    self.isPresenting = operation == .push
    return self
  }
}

//用作模态而不是导航推送弹出做下面 在视图控制器中声明为 属性

    let animator = OptionsViewAnimator()

准备 segue 看起来像

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let dvc = segue.destination
    dvc.transitioningDelegate = animator
}

否则压入和弹出

self.navigationController?.delegate = animator

或设置为零

此外,对于您在导航控制器中的项目 class,它应该如下所示。

func navigationController(_ navigationController: UINavigationController,
                        animationControllerFor operation:   UINavigationControllerOperation,
                        from fromVC: UIViewController,
                        to toVC: UIViewController
) -> UIViewControllerAnimatedTransitioning?
  {
 var animator : OptionsViewAnimator?

if ( toVC   is OptionsViewController && operation == .push ){
    animator = OptionsViewAnimator()
    animator?.isPresenting = true
}else if ( fromVC is OptionsViewController && operation == .pop ){
    animator = OptionsViewAnimator()
    animator?.isPresenting = false
}
 return animator
}

最后(基于 agibson007 提供的示例代码),我发现我的问题是我添加了要作为导航控制器本身的子视图而不是其内容视图推送的视图。解决此问题更正了导航控制器的问题 "forgetting" 如何布局原始视图以适应导航栏和工具栏。

但这也给我留下了我原来的问题,我试图用自定义动画师纠正这个问题——进入选项视图和退出工具栏的丑陋过渡。解决此问题的第一步是将 window 的背景颜色设置为与工具栏的色调相同。这让我完成了 95% 的解决方案。在推送动画结束附近仍然有一个简短的 "blip",其中选项菜单在其出路时低于工具栏。同样的光点出现在流行动画的开始。此处的修复是为工具栏的 alpha 设置动画。它与选项视图的重叠在技术上仍然存在,但我很想见见真正能看到它的人。

我的动画师代码现在看起来像:

import UIKit

class OptionsViewAnimator: NSObject, UIViewControllerAnimatedTransitioning
{
  var type : UINavigationControllerOperation

  let duration = 0.35

  init(operator type:UINavigationControllerOperation)
  {
    self.type = type
  }

  func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
  {
    return self.duration
  }

  func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
  {
    switch type
    {
    case .push: showOptions(using: transitionContext)
    case .pop:  hideOptions(using: transitionContext)
    default:    break
    }
  }

  func showOptions(using context: UIViewControllerContextTransitioning)
  {
    let src        = context.viewController(forKey: .from)!
    let srcView    = context.view(forKey: .from)!
    let dstView    = context.view(forKey: .to)!

    let nav        = src.navigationController
    let toolbar    = nav?.toolbar

    let screen     = UIScreen.main.bounds
    let origin     = srcView.frame.origin
    var dstSize    = srcView.frame.size

    dstSize.height = screen.height - origin.y

    dstView.frame = CGRect(origin: origin, size: dstSize)
    dstView.layer.position = origin
    dstView.layer.anchorPoint = CGPoint(x:0.0,y:0.0)
    dstView.transform = dstView.transform.scaledBy(x: 1.0, y: 0.01)

    let container = context.containerView
    container.addSubview(dstView)

    srcView.window?.backgroundColor = toolbar?.barTintColor
    nav?.setToolbarHidden(true, animated: false)

    UIView.animate(withDuration: self.duration, animations:
      {
        dstView.transform = .identity
        toolbar?.alpha = 0.0
      } )
      {
        (finished)->Void in
        context.completeTransition( !context.transitionWasCancelled )
      }
  }

  func hideOptions(using context: UIViewControllerContextTransitioning)
  {
    let src        = context.viewController(forKey: .from)!
    let srcView    = context.view(forKey: .from)!
    let dstView    = context.view(forKey: .to)!

    let screen     = UIScreen.main.bounds
    let origin     = srcView.frame.origin
    var dstSize    = srcView.frame.size

    let nav        = src.navigationController
    let toolbar    = nav?.toolbar
    let barHeight  = toolbar?.frame.height ?? 0.0

    srcView.layer.anchorPoint = CGPoint(x:0.0,y:0.0)

    dstSize.height = screen.height - (origin.y + barHeight)
    dstView.frame = CGRect(origin:origin, size:dstSize)

    let container = context.containerView
    container.addSubview(dstView)
    container.addSubview(srcView)

    nav?.setToolbarHidden(false, animated: false)
    toolbar?.alpha = 0.0

    UIView.animate(withDuration: 0.35, animations:
      {
        srcView.transform = srcView.transform.scaledBy(x: 1.0, y: 0.01)
        toolbar?.alpha = 1.0
      } )
      {
        (finished)->Void in
        context.completeTransition( !context.transitionWasCancelled )
      }
  }
}