Как я могу направить контроллер представления на стек навигатора из результатов поиска, когда вы представляете их модально?

Я хочу воссоздать пользовательский интерфейс поиска, показанный в приложении календаря iOS 7/8. Представление пользовательского интерфейса поиска не является проблемой. Я использую UISearchController и модно представляю его так же, как показывает пример кода UICatalog, который дает мне приятную анимацию. Проблема возникает при попытке нажать контроллер вида из контроллера представления результатов. Он не завернут в контроллер навигации, поэтому я не могу на него надавить. Если я завершу его в контроллер навигации, тогда я не получаю анимацию с уменьшением по умолчанию, когда я представляю UISearchController. Любые идеи?

EDIT: Я получил его, чтобы нажимать, завернув мой контроллер представления результатов в навигационном контроллере. Однако панель поиска по-прежнему присутствует после нажатия нового VC на стек.

ИЗМЕНИТЬ (2): DTS от Apple заявила, что приложение календаря использует нестандартный метод для вывода результатов поиска. Вместо этого они рекомендуют удалять фокус с контроллера поиска, а затем нажимать и возвращать фокус на поп. Это похоже на способ поиска в приложениях настроек, которые я себе представляю.

Ответ 1

Apple стала очень умной, но это не толчок, хотя он выглядит как один.

Они используют настраиваемый переход (аналогично тому, что делает навигационный контроллер), чтобы сползать в контроллер представления, встроенный в контроллер навигации.

Вы можете определить разницу, медленно развернув края, чтобы просмотреть подробный вид назад и позволить начать предыдущий просмотр. Обратите внимание на то, как верхняя панель навигации сдвигается вправо вместе с деталями, а не ее кнопки и заголовок на месте?

Update:

Проблема, которую вы видите, заключается в том, что контроллер поиска представлен над вашим навигационным контроллером. Как вы обнаружили, даже если вы нажмете контроллер вида на стек навигационного контроллера, панель навигации все еще находится под представлением контроллера поиска, поэтому панель поиска заслоняет любую навигационную панель (сдвинутого контроллера).

Если вы хотите показывать результаты на вершине контроллера поиска, не отпуская его, вам нужно будет представить свой собственный диспетчер вида навигации.

К сожалению, нет стиля перехода, который позволит вам представить свой контроллер навигации так же, как ведет себя встроенная анимация push.

Как я вижу, есть три эффекта, которые необходимо дублировать.

  • Ниже приведено содержание нижележащего содержимого, поскольку представленный вид появляется.
  • Представленный вид имеет тень.
  • Навигация основного контента полностью анимируется вне экрана, но его контент частично оживляет.

Я воспроизвел общий эффект в рамках интерактивного пользовательского модального перехода. Обычно он имитирует анимацию календаря, но есть некоторые отличия (не показаны)), например, клавиатура (re) появляется слишком рано.

Модальный контроллер, который представляет собой навигационный контроллер. Я подключил кнопку "Назад" и "Жест по краю", чтобы (в интерактивном режиме) отклонить его.

введите описание изображения здесь

Ниже приведены шаги, которые будут выполняться:

  • В вашей раскадровке вы измените тип Segue с Показать детали на Показывать в режиме.

Вы можете оставить Презентация и Переход по умолчанию, так как они должны быть переопределены в коде.

  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 (желтый куб) из библиотеки объектов в модовой навигационный контроллер. Установите его класс в наш NavigationControllerDelegate и подключите его выходы delegate и navigationController к модульному навигационному контроллеру раскадровки.

  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
    }
    

Код анимации слишком длинный, чтобы обеспечить ответ, но он доступен в примерном проекте, который я поделил в GitHub.

Сказав это, код, поскольку он стоит, был более интересным упражнением. Apple уже много лет совершенствует и поддерживает все свои переходы. Если вы примете эту пользовательскую анимацию, вы можете найти случаи (например, видимую клавиатуру), где анимация не делает то, что делает Apple. Вам нужно будет решить, хотите ли вы потратить время на улучшение кода, чтобы правильно обрабатывать эти случаи.

Ответ 2

Я знаю, что эта ветка устарела, но, похоже, гораздо более простой подход к получению желаемого поведения.

Важно понять, что UISearchController представлен с помощью контроллера источника, который является контроллером представления внутри навигационного контроллера. Если вы проверите иерархию представлений, вы увидите, что контроллер поиска, в отличие от обычных модальных презентаций, не представлен как прямой дочерний элемент окна, а скорее как подсмотр диспетчера навигации.

Итак, общая структура

  • UINavigationController
    • MyRootViewController
      • UISearchViewController (представлен псевдо-модально)
        • MyContentController

По существу вам просто нужно перейти с MyContentController до MyRootViewController, чтобы вы могли получить доступ к свойству navigationController. В моем методе tableView:didSelectRowAtIndexPath: моего контроллера содержимого для поиска я просто использую следующее для доступа к контроллеру корневого представления.

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

Оттуда вы можете легко нажать что-то на контроллер навигации, а анимация - именно то, что вы ожидаете.

Ответ 3

EDIT: альтернативное решение, которое не полагается на UIWindow. Я думаю, что эффект очень похож на приложение календаря.

@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 класс. Когда результат поиска отображается, он создает новый UIWindow с корневым UINavigationController и подталкивает к нему контроллер представления результатов. Он следит за появлением UINavigationController для root, поэтому он может отклонить специальный UIWindow.

Теперь UIWindow строго не требуется. Я использовал его, потому что это помогает сохранить видимость SearchViewController во время перехода push/pop. Вместо этого вы можете просто представить UINavigationController из UISearchController (и отклонить его из метода navigationController: didShowViewController: delegate). Но модально представленные контроллеры представления присутствуют по непрозрачному виду по умолчанию, скрывая то, что внизу. Вы можете обратиться к этому, написав пользовательский переход, который будет применяться в качестве 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

Ответ 4

Когда вы представляете viewController, navigationController становится недоступным. Поэтому сначала вам нужно отклонить свой модальный, а затем нажать еще один viewController.

Ответ 5

UISearchController должен быть rootViewController UINavigationController, а затем вы представляете навигационный контроллер как модальный.