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

Posted

技术标签:

【中文标题】UINavigationController 在自定义过渡动画后忘记如何旋转子控制器【英文标题】:UINavigationController forgets how to rotate sub controllers after custom transition animation 【发布时间】:2017-02-27 06:40:56 【问题描述】:

我这几天一直在解决以下问题。我在文档中找不到关于我做错了什么的任何内容。我已经爬过调试器并将各种信息转储到控制台,但无法弄清楚如何修复我所看到的内容。

我的***视图控制器是 UINavigationController 的子类。这包含 UIPageViewController 的子类,它将用于可视化我的数据。它还包含 UITableViewController 的子类,用于设置应用程序设置。这个“选项”视图通过 push segue “链接”到页面视图控制器。至此,一切正常。

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

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

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


以下显示的调试屏幕截图说明了 5s 模拟器上的问题

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

    将方向旋转为水平。到目前为止一切正常。调试控制台显示视图正在“旋转”到 (568x212) 的大小、状态、导航和工具栏的前后高度、屏幕大小、框架矩形和图层位置。我这里没有展示,但是图层锚点永远不会从 (0.5, 0.5) 改变。

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

rot_width(568) = old_frame_height(460) + sum_of_bar_heights(108) rot_height(212) = old_frame_width(320) - sum_of_bar_heights(108)

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

new_frame_width(568) = rot_width(568) new_frame_height(256) = rot_height(212) + change_in_bar_height(108-64)

结果帧原点 (0,64) 与屏幕顶部的偏移量为状态栏 (20) 和导航栏 (44) 之和。

    将方向旋转回垂直。再次,一切正常。调试控制台为“反向”旋转添加了与上述相同的信息。旋转大小和生成的帧大小遵循与上述相同的规则。

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

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

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

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

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

BUT,轮换的结果和第1步不同。规则好像变了。生成的框架大小不再根据状态、导航和工具栏高度的变化进行调整,并且框架原点与旋转之前的状态相比没有变化。

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


这里显示了我的动画师类(完整的,除了用于生成上面显示的调试信息的探针):

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 )
    
  

感谢您的任何/所有帮助。 麦克

【问题讨论】:

只是好奇是否没有人知道答案,或者我的问题是否存在根本性的问题而没有人参与。 给我一点,我会试着权衡一下。这是一个非常高级的问题。我以前也遇到过,所以我去看看代码,看看问题。 请查看答案并让我知道它是否有效。如果是这样,请标记正确。干杯 【参考方案1】:

既然您希望它覆盖 UINavigation 控制器栏,是否有任何理由不使用自定义模态(以模态形式出现在情节提要中)。无论哪种方式,我都将它构建为与两者都远离添加到窗口。测试后告诉我,但在我的测试中,它与任何一个都完美。

 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()

Prepare for segue 看起来像

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

否则推送和弹出

self.navigationController?.delegate = animator

或设置为零

另外,对于导航控制器类中的项目,它应该如下所示。

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

【讨论】:

我最初将此标记为答案,但发现它仍然存在缺陷。选项视图在导航栏和工具栏下方延伸。我希望视图的顶部紧靠导航 var 的底部和要覆盖的工具栏。此外,当显示原始视图时,它现在也位于导航栏下方,切断了内容的顶部。 (我的虚拟内容不是问题,但是当我开始用真实的东西填充这个视图时,这将是一个问题。 我将我的代码与您的动画师版本一起上传到了 github:github.com/mikemayer67/truCourse/tree/stack_overflow_suggestion 还有其他具有各种级别的调试语句并尝试解决问题的分支。 我明白了。您是否尝试将其作为模态演示文稿来代替? 模态显示完全隐藏了导航栏。这让我陷入了选项视图,因为返回按钮(更新或取消)位于导航栏上。我怀疑如果我选择你的演示方法,我可能会首先发现我做错了什么。但是,我现在需要开始我真正的工作。今晚晚些时候我会再谈这个。 小心给答案试运行。【参考方案2】:

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

但这也给我留下了最初的问题,我试图用自定义动画器来纠正 - 输入选项视图和退出工具栏的丑陋过渡。解决此问题的第一步是将窗口的背景颜色设置为工具栏的颜色。这给我带来了 95% 的解决方案。在推送动画结束时仍然有一个简短的“光点”,选项菜单在工具栏退出时位于工具栏下方。流行动画开始时出现了相同的光点。这里的修复是为工具栏的 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 )
      
  

【讨论】:

您的问题是您正在添加到窗口中。您还需要在返回时使用最终帧来处理旋转。

以上是关于UINavigationController 在自定义过渡动画后忘记如何旋转子控制器的主要内容,如果未能解决你的问题,请参考以下文章

如何避免在自定义容器(stackedViewcontroller)内的 UiNavigationcontroller 内调整 UITableView 的大小

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

UINavigationController 的自定义动画推送未正确呈现导航栏

在自定义视图控制器转换中抑制导航推送动画

根视图控制器和模式对话框

关闭 UINavigationController 并呈现另一个 UINavigationController