0

I have been fighting the following problem for a couple days now. I cannot find anything in the documentation about what I am doing wrong. I have crawled through the debugger and have dumped all sorts of info to the console, but cannot get my head around how to fix what I'm seeing.

My top level view controller is a subclass of UINavigationController. This contains a subclass of UIPageViewController which will be used to visualize my data. It also contains a subclass of UITableViewController for setting app settings. This "options" view is "linked" to the page view controller via a push segue. At this point, everything worked fine.

Problems started when I wanted to differentiate the visual transition between the various ways of visualizing the data (a horizontal scroll in the page view controller) and between bringing in the option view. So, I created a custom animation to "stretch in" the options controller from the top of the screen (on push) or "roll up" the options controller to the top of the screen (on pop). I set this animation on the navigation controller using the navigationController:animationControllerFor:operation:from:to: method.

Before displaying the options controller everything seems to work fine during device rotation. After displaying/dismissing the options controller, the layout of the data visualization controller breaks down during rotation. It displays correctly in the orientation that the device was in when the options controller was dismissed, but played out incorrectly in the opposite orientation.

It appears that the navigation controller (or possibly the page view controller) has forgotten how to handle accounting for the height of the status, navigation, and toolbar.


The following shows debugging screenshots illustrates the issue on a 5s simulator

  1. Initial state. Note that the content view (red) is below the status, navigation, and tool bars.

enter image description here

  1. Rotated the orientation to horizontal. So far everything is ok. The debug console shows the size the view is being "rotated" to (568x212), the before/after heights of the status, navigation, and tool bars, the screen size, the frame rectangle, and the layer position. I have not shown it here, but the layer anchor point never changes from (0.5, 0.5).

Note that the target rotation size is set using the following rules:

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

And the resulting frame size is set using the following rules:

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

And the resulting frame origin (0,64) is offset from the top of the screen by the sum of the status bar(20) and the navigation bar(44).

enter image description here

  1. Rotated the orientation back to vertical. Again, everything is ok. The debug console adds the same information as above for the "reverse" rotation. The rotation size and resulting frame size follow the same rules as above.

enter image description here

  1. Pushed the option editor controller view onto the navigation stack using a custom animation. The debug console adds the same information as above for the (currently not visible) content view as above. Note that nothing has changed.

enter image description here

  1. Popped the option editor controller view off of the navigation stack using a custom animation. The content view from above returns to view. The debug console adds the same information as above. Again, nothing has changed.

Note, The stacking order is different from before the option editor was made visible.

This stack re-ordering does NOT hold when using the default transition animator (by returning nil from pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted)

enter image description here

  1. Repeat of step 1. This is where things being to go "wonky." The content view no longer fits snuggly against the navigation and tool bars. The debug console shows that everything going into the rotation looks the same as in step 1.

BUT, the result of the rotation is different from step 1. The rules seem to have changed. The resulting frame size is no longer adjusted to account for changes in status, navigation, and toolbar heights and the frame origin does not change from what it was before the rotation.

enter image description here

  1. Repeat of step 2. It appears that everything is fixed, but that is only because it is playing by the new resizing rules.

enter image description here


My animator class is shown (in its entirety, except for the probes used to produce the debugging info shown above) here:

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

Thanks for any/all help. mike

4

2 回答 2

1

Since you want it to cover the UINavigation Controller Bar is there any reason not to use a custom modal(present modally in storyboard). Either way I built it to work with both staying away from adding to the window. Let me know after you test but in my test it works perfect with either.

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

//To use as modal instead of navigation push pop do below Declare as property in view controller

    let animator = OptionsViewAnimator()

And Prepare for segue would look like

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

Otherwise to push and pop

self.navigationController?.delegate = animator

or set to nil

Additionally for your project in the navigation controller class it should look like this.

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
}
于 2017-03-06T21:59:17.147 回答
0

In the end (based on example code provided by agibson007), I discovered that my problem was that I was adding my view to be pushed as a subview of the navigation controller itself rather than its content view. Fixing this corrected the issue of the navigation controller "forgetting" how to lay out the original view to fit between the nav bar and toolbar.

But this also left me with my original issue that I was trying to correct with a custom animator—an ugly transition with the entering options view and the exiting toolbar. First step to fixing this was to set the window's background color equal to the toolbar's tint. That brought me 95% of the way to a solution. There was still a brief "blip" near the end of the push animation where the option menu underlapped the toolbar on its way out. Same blip appeared at the start of pop animations. The fix here was to animate the toolbar's alpha. Its overlap of the option view is technically still there, but I would love to meet the person who can actually see this.

My animator code now looks like:

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 )
      }
  }
}
于 2017-03-09T13:55:18.613 回答