Search code examples
iosios8uinavigationcontrolleruisearchcontroller

How do I push a view controller onto the nav stack from search results when presenting them modally?


I want to recreate the search UI shown in the iOS 7/8 calendar app. Presenting the search UI modally isn't a problem. I use UISearchController and modally present it just like the UICatalog sample code shows which gives me a nice drop down animation. The issue comes when trying to push a view controller from the results view controller. It isn't wrapped in a navigation controller so I can't push onto it. If I do wrap it in a navigation controller then I don't get the default drop down animation when I present the UISearchController. Any ideas?

EDIT: I got it to push by wrapping my results view controller in a nav controller. However the search bar is still present after pushing the new VC onto the stack.

EDIT (2): DTS from Apple said that the calendar app uses a non-standard method to push from search results. Instead they recommend removing focus from the search controller then pushing and returning focus on pop. This is similar to the way search in the settings app works I imagine.


Solution

  • Apple has gotten very clever there, but it's not a push, even though it looks like one.

    They're using a custom transition (similar to what a navigation controller would do) to slide in a view controller which is embedded in a navigation controller.

    You can spot the difference by slowly edge-swiping that detail view back and letting the previous view start to appear. Notice how the top navigation slides off to the right along with the details, instead of its bar buttons and title transitioning in-place?

    Update:

    The problem that you're seeing is that the search controller is presented above your navigation controller. As you discovered, even if you push a view controller onto a navigation controller's stack, the navigation bar is still beneath the search controller's presentation, so the search bar obscures any (pushed view controller's) navigation bar.

    If you want to show results on top of the search controller without dismissing it, you'll need to present your own modal navigation view controller.

    Unfortunately, there's no transition style which will let you present your navigation controller the same way the built-in push animation behaves.

    As I can see, there are three effects that need to be duplicated.

    1. The underlying content dims, as the presented view appears.
    2. The presented view has a shadow.
    3. The underlying content's navigation completely animates off-screen, but its content partially animates.

    I've reproduced the general effect within an interactive custom modal transition. It generally mimic's Calendar's animation, but there are some differences (not shown), such as the keyboard (re)appearing too soon.

    The modal controller that's presented is a navigation controller. I wired up a back button and edge swipe gesture to (interactively) dismiss it.

    enter image description here

    Here are the steps that are involved:

    1. In your Storyboard, you would change the Segue type from Show Detail to Present Modally.

    You can leave Presentation and Transition set to Default, as they'll need to be overridden in code.

    1. In Xcode, add a new NavigationControllerDelegate file to your project.

      NavigationControllerDelegate.h:

      @interface NavigationControllerDelegate : NSObject <UINavigationControllerDelegate>
      

      NavigationControllerDelegate.m:

      @interface NavigationControllerDelegate () <UIViewControllerTransitioningDelegate>
      @property (nonatomic, weak) IBOutlet UINavigationController *navigationController;
      @property (nonatomic, strong) UIPercentDrivenInteractiveTransition* interactionController;
      @end
      
      - (void)awakeFromNib
      {
          UIScreenEdgePanGestureRecognizer *panGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
          panGestureRecognizer.edges = UIRectEdgeLeft;
      
          [self.navigationController.view addGestureRecognizer:panGestureRecognizer];
      }
      
      #pragma mark - Actions
      
      - (void)handlePan:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer
      {
          UIView *view = self.navigationController.view;
      
          if (gestureRecognizer.state == UIGestureRecognizerStateBegan)
          {
              if (!self.interactionController)
              {
                  self.interactionController = [UIPercentDrivenInteractiveTransition new];
                  [self.navigationController dismissViewControllerAnimated:YES completion:nil];
              }
          }
          else if (gestureRecognizer.state == UIGestureRecognizerStateChanged)
          {
              CGFloat percent = [gestureRecognizer translationInView:view].x / CGRectGetWidth(view.bounds);
              [self.interactionController updateInteractiveTransition:percent];
          }
          else if (gestureRecognizer.state == UIGestureRecognizerStateEnded)
          {
              CGFloat percent = [gestureRecognizer translationInView:view].x / CGRectGetWidth(view.bounds);
              if (percent > 0.5 || [gestureRecognizer velocityInView:view].x > 50)
              {
                  [self.interactionController finishInteractiveTransition];
              }
              else
              {
                  [self.interactionController cancelInteractiveTransition];
              }
              self.interactionController = nil;
          }
      }
      
      #pragma mark - <UIViewControllerAnimatedTransitioning>
      
      - (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)__unused presented presentingController:(UIViewController *)__unused presenting sourceController:(UIViewController *)__unused source
      {
          TransitionAnimator *animator = [TransitionAnimator new];
          animator.appearing = YES;
          return animator;
      }
      
      - (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)__unused dismissed
      {
          TransitionAnimator *animator = [TransitionAnimator new];
          return animator;
      }
      
      - (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)__unused animator
      {
          return nil;
      }
      
      - (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)__unused animator
      {
      #pragma clang diagnostic push
      #pragma clang diagnostic ignored "-Wgnu-conditional-omitted-operand"
          return self.interactionController ?: nil;
      #pragma clang diagnostic pop
      }
      

      The delegate will provide the controller with its animator, interaction controller, and manage the screen edge pan gesture to dismiss the modal presentation.

    2. In Storyboard, drag an Object (yellow cube) from the object library to the modal navigation controller. Set its class to ourNavigationControllerDelegate, and wire up its delegate and navigationController outlets to the storyboard's modal navigation controller.

    3. In prepareForSegue from your search results controller, you'll need to set the modal navigation controller's transitioning delegate and modal presentation style.

      navigationController.transitioningDelegate = (id<UIViewControllerTransitioningDelegate>)navigationController.delegate;
      navigationController.modalPresentationStyle = UIModalPresentationCustom;
      

      The custom animation that the modal presentation performs is handled by transition animator.

    4. In Xcode, add a new TransitionAnimator file to your project.

      TransitionAnimator.h:

      @interface TransitionAnimator : NSObject <UIViewControllerAnimatedTransitioning>
      @property (nonatomic, assign, getter = isAppearing) BOOL appearing;
      

      TransitionAnimator.m:

      @implementation TransitionAnimator
      @synthesize appearing = _appearing;
      
      #pragma mark - <UIViewControllerAnimatedTransitioning>
      
      - (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
      {
          return 0.3;
      }
      
      - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
      {
          // Custom animation code goes here
      }
      

    The animation code is too long to provide within an answer, but it's available in a sample project which I've shared on GitHub.

    Having said this, the code, as it stands, was more of a fun exercise. Apple has had years to refine and support all their transitions. If you adopt this custom animation, you may find cases (such as the visible keyboard) where the animation doesn't do what Apple's does. You'll have to decide whether you want to invest the time to improve the code to properly handle those cases.