以模态方式呈现时,如何将视图控制器从搜索结果推送到导航堆栈?

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

我想重新创建 iOS 7/8 日历应用程序中显示的搜索 UI。以模态方式显示搜索 UI 不是问题。我使用 UISearchController 并以模式方式呈现它,就像 UICatalog 示例代码显示的那样,它给了我一个很好的下拉动画。尝试从结果视图控制器推送视图控制器时出现问题。它没有包含在导航控制器中,所以我无法推动它。如果我确实将它包装在导航控制器中,那么当我显示 UISearchController 时,我不会获得默认的下拉动画。有什么想法吗?

编辑: 我通过将我的结果视图控制器包装在导航控制器中来推动它。然而,在将新 VC 推入堆栈后,搜索栏仍然存在。

编辑(2): Apple 的 DTS 表示,日历应用程序使用非标准方法从搜索结果中推送。相反,他们建议从搜索控制器中移除焦点,然后将焦点推送并返回到 pop。这类似于我想象中的设置应用程序中的搜索方式。

当您呈现 viewController 时,navigationController 将变得不可用。所以你必须先关闭你的模态,然后再推另一个 viewController.

UISearchController 必须是 UINavigationController 的 rootViewController,然后您将导航控制器呈现为模态。

Apple 在这方面做得非常聪明,但这并不是推动,尽管它看起来像一个。

他们使用自定义转换(类似于导航控制器所做的)在嵌入在导航控制器中的视图控制器中滑动。

您可以通过慢慢 edge-swiping 返回详细视图并让之前的视图开始出现来发现差异。请注意顶部导航如何与详细信息一起向右滑动,而不是其栏按钮和标题过渡 in-place?

更新:

您看到的问题是搜索控制器显示在导航控制器上方。正如您所发现的,即使您将视图控制器推送到导航控制器的堆栈上,导航栏仍位于搜索控制器的显示下方,因此搜索栏会遮挡任何(推送的视图控制器的)导航栏。

如果您想在搜索控制器顶部显示结果而不关闭它,您需要展示您自己的模态导航视图控制器。

不幸的是,没有过渡样式可以让您以与 built-in 推送动画行为相同的方式呈现导航控制器。

如我所见,需要复制三个效果。

  1. 随着呈现的视图出现,基础内容变暗。
  2. 呈现的视图有阴影。
  3. 底层内容的导航完全动画化 off-screen,但其内容部分动画化。

I've reproduced the general effect within an interactive custom modal transition. 大体模仿日历的动画,但也有一些差异(未显示),例如键盘(重新)出现得太快。

显示的模态控制器是一个导航控制器。我连接了一个后退按钮和边缘滑动手势以(交互地)关闭它。

以下是涉及的步骤:

  1. 在您的 Storyboard 中,您可以将 Segue 类型从 Show Detail 更改为 Present Modally.

您可以将 PresentationTransition 设置为默认值,因为它们需要在代码中被覆盖。

  1. 在 Xcode 中,将新的 NavigationControllerDelegate 文件添加到您的项目。

    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
    }
    

    委托将为控制器提供其动画器、交互控制器,并管理屏幕边缘平移手势以关闭模态呈现。

  2. 在 Storyboard 中,将 Object(黄色立方体)从 object 库拖到模态导航控制器。将其 class 设置为我们的 NavigationControllerDelegate,并将其 delegatenavigationController 出口连接到故事板的模态导航控制器。

  3. 在搜索结果控制器的 prepareForSegue 中,您需要设置模态导航控制器的转换委托和模态呈现样式。

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

    模式呈现执行的自定义动画由过渡动画师处理。

  4. 在 Xcode 中,将新的 TransitionAnimator 文件添加到您的项目中。

    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
    }
    

动画代码太长,无法在答案中提供,但在 a sample project which I've shared on GitHub 中可用。

话虽如此,就目前而言,代码更像是一个有趣的练习。 Apple 多年来一直在完善和支持他们的所有过渡。如果您采用此自定义动画,您可能会发现动画无法执行 Apple 所执行的操作的情况(例如可见键盘)。您必须决定是否要花时间改进代码以正确处理这些情况。

EDIT:不依赖于 UIWindow 的替代解决方案。我觉得效果和日历app很像。

@interface SearchResultsController () <UINavigationControllerDelegate>
@end

@implementation SearchResultsController

- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // this will be the UINavigationController that provides the push animation.
    // its rootViewController is a placeholder that exists so we can actually push and pop
    UIViewController* rootVC = [UIViewController new]; // this is the placeholder
    rootVC.view.backgroundColor = [UIColor clearColor];
    UINavigationController* nc = [[UINavigationController alloc] initWithRootViewController: rootVC];
    nc.modalPresentationStyle = UIModalPresentationCustom;
    nc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;

    [UIView transitionWithView: self.view.window
                      duration: 0.25
                       options: UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowAnimatedContent
                    animations: ^{

                        [self.parentViewController presentViewController: nc animated: NO completion: ^{

                            UIViewController* resultDetailViewController = [UIViewController alloc];
                            resultDetailViewController.title = @"Result Detail";
                            resultDetailViewController.view.backgroundColor = [UIColor whiteColor];

                            [nc pushViewController: resultDetailViewController animated: YES];
                        }];
                    }
                    completion:^(BOOL finished) {

                        nc.delegate = self;
                    }];
}

- (void) navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    // pop to root?  then dismiss our window.
    if ( navigationController.viewControllers[0] == viewController )
    {
        [UIView transitionWithView: self.view.window
                          duration: [CATransaction animationDuration]
                           options: UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowAnimatedContent
                        animations: ^{

                            [self.parentViewController dismissViewControllerAnimated: YES completion: nil];
                        }
                        completion: nil];
    }
}

@end

原始解决方案:

这是我的解决方案。我开始使用您在 UICatalog 示例中发现的相同技术来显示搜索控制器:

- (IBAction)search:(id)sender
{
    SearchResultsController* searchResultsController = [self.storyboard instantiateViewControllerWithIdentifier: @"SearchResultsViewController"];

    self.searchController = [[UISearchController alloc] initWithSearchResultsController:searchResultsController];
    self.searchController.hidesNavigationBarDuringPresentation = NO;


    [self presentViewController:self.searchController animated:YES completion: nil];
}

在我的示例中,SearchResultsController 是派生的 UITableViewController class。当点击搜索结果时,它会创建一个带有根 UINavigationController 的新 UIWindow 并将结果详细信息视图控制器推送到它。它监视弹出到根的 UINavigationController,以便它可以关闭特殊的 UIWindow。

现在,UIWindow 并不是严格要求的。我使用它是因为它有助于在 push/pop 转换期间保持 SearchViewController 可见。相反,您可以只显示 UISearchController 中的 UINavigationController(并从 navigationController:didShowViewController: 委托方法中将其关闭)。但是模态呈现的视图控制器默认情况下呈现在不透明的视图上,隐藏了下面的内容。您可以通过编写将作为 UINavigationController 的 transitioningDelegate 应用的自定义转换来解决此问题。

    @interface SearchResultsController () <UINavigationControllerDelegate>
@end

@implementation SearchResultsController
{
    UIWindow* _overlayWindow;
}

- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // this will be the UINavigationController that provides the push animation.
    // its rootViewController is a placeholder that exists so we can actually push and pop
    UINavigationController* nc = [[UINavigationController alloc] initWithRootViewController: [UIViewController new]];

    // the overlay window
    _overlayWindow = [[UIWindow alloc] initWithFrame: self.view.window.frame];
    _overlayWindow.rootViewController = nc;
    _overlayWindow.windowLevel = self.view.window.windowLevel+1; // appear over us
    _overlayWindow.backgroundColor = [UIColor clearColor];
    [_overlayWindow makeKeyAndVisible];

    // get this into the next run loop cycle:
    dispatch_async(dispatch_get_main_queue(), ^{

        UIViewController* resultDetailViewController = [UIViewController alloc];
        resultDetailViewController.title = @"Result Detail";
        resultDetailViewController.view.backgroundColor = [UIColor whiteColor];
        [nc pushViewController: resultDetailViewController animated: YES];

        // start looking for popping-to-root:
        nc.delegate = self;
    });
}

- (void) navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    // pop to root?  then dismiss our window.
    if ( navigationController.viewControllers[0] == viewController )
    {
        [_overlayWindow resignKeyWindow];
        _overlayWindow = nil;
    }
}

@end

我知道这个帖子很旧,但似乎有一种更简单的方法来获得所需的行为。

要认识到的重要一点是 UISearchController 是从源控制器呈现的,它是导航控制器内的视图控制器。如果您检查视图层次结构,您会发现搜索控制器与常规模式呈现不同,它不是作为 window 的直接子视图呈现,而是作为导航控制器的子视图呈现。

所以大体结构是

  • UINavigationController
    • MyRootViewController
      • UISearchViewController(呈现伪"modally")
        • MyContentController

基本上你只需要从 MyContentController 到 MyRootViewController,这样你就可以访问它的 navigationController 属性。在我的搜索内容控制器的 tableView:didSelectRowAtIndexPath: 方法中,我只需使用以下内容来访问我的根视图控制器。

UINavigationController *navigationController = nil;
if ([self.parentViewController isKindOfClass:[UISearchController class]]) {
    navigationController = self.parentViewController.presentingViewController.navigationController;
}

从那里您可以轻松地将某些内容推送到导航控制器上,动画正是您所期望的。