Нативный segue слева направо в iOS

    Предупреждаю сразу, это трюк. Он подойдёт далеко не всем и не всегда, но если вам нужно вывести окно с какой-то информацией слева от основного — то мой способ будет в самый раз.

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

    Для начала, немного вводных данных. Segue — это способ смены экранов в iOS. Одна из самых популярных разновидностей, это push (с версии iOS 8 — show). Push segue всегда замещает текущий вид справа налево. То есть, у вас как-бы справа есть второе окно, и при нажатии кнопки оно переезжает налево, замещая первое.

    Такое поведение вы видите в телефонной книге при выборе абонента. При этом, вверху появляется кнопка возврата на предыдущее окно, и, при нажатии на него, происходит искомая анимация слева направо.

    Проблема в том, что мне нужно было сделать всё тоже самое, но в зеркальном отражении и стандартных способов для этого не существует (если верить Google). Есть масса инструкций, как сделать custom segue с похожей анимацией, но все они режут глаз своей неестественностью по сравнению с родным push.

    Вот, что у меня получилось в итоге:



    Используются исключительно стандартные методы и 0% кастомной анимации. Готовый пример на GitHub.

    Идея


    Основной трюк заключается в том, что мы не делаем никакого нового segue, мы используем родной push, по назначению.

    Думаю, уже по этому скриншоту понятен смысл. Стартовое окно — Info, которое нам нужно выводить слева. При его активации мы быстро и незаметно, без анимации, выводим окно Main при помощи push segue. А после этого уже можно вернуться обратно, используя unwindFromViewController. Только пользователь об этом не знает и видит плавный и красивый left to right segue.

    Реализация


    Не буду расписывать как соединять друг с другом элементы и назначать кастомные классы, уверен — вы все и так это знаете. В любом случае, можно скачать исходники с GitHub и посмотреть.

    Нам понадобятся 2 иконки, один custom segue и один InfoViewController.

    На окошко Info мы добавляем одну кнопку с иконкой домика, справа. Затем соединяем её с окном Main обычным push (show) segue.

    Создаём кастомный класс InfoViewController, наследник UIViewController и назначаем его окошку Info:
    #import "InfoViewController.h"
    
    @interface InfoViewController ()
    
    @end
    
    @implementation InfoViewController
    
        // Быстрая перемотка на окно Main при первом запуске
        // Запускаем custom segue с идентификатором FastSegue
    	- (void) viewDidLoad {
    		[self performSegueWithIdentifier:@"FastSegue" sender:nil];
    	}
    
        // Указание метода для возврата на окно Info
    	- (IBAction)unwindFromViewController:(UIStoryboardSegue *)sender {
    	}
    
    @end
    


    Теперь нужно создать custom segue, который будет незаметно выводить окно Main при старте приложения:
    #import "FastSegue.h"
    
    @implementation FastSegue
    
        // Обычный push segue, только без анимации
    	- (void) perform {
    		[[[self sourceViewController] navigationController] pushViewController:[self destinationViewController] animated:NO];
    	}
    
    @end
    

    А теперь нужно создать связать этим custom segue контроллер Info с Main. Этой связи обязательно присваиваем идентификатор FastSegue.



    Дело за малым — сделать возврат на окно Info при нажатии на кнопку окошка Main. Это необязательная часть, потому что, по умолчанию, там и так будет сгенерирована кнопка возврата со стрелкой и названием предыдущего окна, но мне нужна именно иконка.
    Кроме того, как оказалось, если заменить backBarButtonItem своим элементом, то нельзя открыть окно Info жестом слева направо, как при обычном segue.

    Итак, добавляем на окно Main элемент UINavigationItem и leftBarButtonItem, чтобы заменить автоматически генерирующийся родной backBarButtonItem. Картинкой выбираем иконку «i» и наша кнопка успешно показывается при старте приложения. Только ничего не делает.

    Для того, чтобы она заменила собой функционал backBarButtonItem, нужно назначить ей action unwindFromViewController — именно для этого мы и обозначили его в контроллере окна Info.

    Так что связываем кнопку с точкой выхода и выбираем unwindFromViewController:

    Вот и всё!
    Добавлю еще, что для незаметного стартового переключения нужна LaunchImage, или LaunchScreen, но приложений без них и так не бывает.

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

    Спасибо за внимание.

    Updated

    Заменил старое видео на чуть более наглядное, с таблицами.

    Updated 2

    Как оказалось, никакой это не костыль, никакое это не «необычное поведение». Точно так же работает родное приложение Mail.

    Открываем Mail — видим сразу письма и вверху кнопку на открытие всех ящиков. При нажатии на неё происходит left to right segue, скорее всего, точно так же сделанный. То есть, предварительной незаметной отмоткой на второе окно.

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 21

    • UFO just landed and posted this here
        0
        Это ж не моя причуда, это воля заказчика.

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

        Но, на мой взгляд, здесь нет никакого криминала — так только лучше выглядит и понятнее работает. Правая кнопка открывает окно справа, левая — слева. Дальше по ходу программы так везде (потому что все push только направо, а возвраты налево =)).
        • UFO just landed and posted this here
        0
        Довольно логичное и очевидное, на мой взгляд, решение, хотя и кажется чем-то костыльным, но раз воля заказчика… :) Главное помнить потом, если понадобится вдруг куда-то сильно назад вернуться, что первый ViewController, который видит пользователь, не первый по очереди.

        Вопрос — а когда вы пушите через pushViewController принудительно кодом — в ваш InfoViewController можно же вернуться простым жестом, проведя пальцем от левого края экрана вправо — это будет по идее эквивалентно нажатию кнопки «назад». Это не сломает случайно ваш User Experience? :)
          –1
          Нет, жест не срабатывает из-за того, что backBarButtonItem заменён своей кнопкой.

          Если оставить стандартную — то жест включается, а так нет.
          +1
          У вас пример с белыми окнами, там не заметно, но как это выглядит в приложении? У вас же не левое окно выезжает слева, а «основное» окно улетает вправо. Или до таких мелочей заказчику нет дела? :)

          Не может быть такого, что улетевшее вправо «основное» окно вдруг уничтожится из-за нехватки памяти?
            –1
            Отлично выглядит, это же обычный push segue. Абсолютно одинаковая зеркальная анимация в обе стороны.

            Вот, смотрите, добавил таблицы.

            Если это окно «вдруг уничтожится», то такая незавидная участь может постигнуть любое другое окно в приложении, потому что эти 2 от них ничем не отличаются.
              0
              Видео с ограниченным доступом
                +1
                Ай, поправил!
                +1
                В таблицах горизонтальные линии, на них ничего не видно :)

                Текущее окно сдвигается влево где-то на треть, а новое окно выезжает полностью.
                В обратную сторону аналогично — «верхнее» окно улетает вправо целиком, а «нижнее» в начале анимации сдвинуто на треть влево и движется всего на треть влево.
                Анимация не зеркальная.
                Видео с картинками: de.tinypic.com/r/vpg7qr/8 (для просмотра нужен Flash, открывайте в Google Chrome)
                Следите за анимацией — левое окно начинает/заканчивает движение в районе кисти человека.

                Но это всё придирки — заказчик и большинство пользователей не заметят подвоха :)
                  0
                  В третьем предложении последнее слово должно быть «вправо», а не «влево», конечно же.
                    0
                    Но это всё придирки — заказчик и большинство пользователей не заметят подвоха
                    Да я, в общем-то, и сам не замечаю подвоха.

                    У вас есть свой, более правильный, вариант left to right segue?
                0
                Объясню, почему поставил минус: вы понимаете, что сделали плохо (прикрываясь волей заказчика), и, тем не менее, «учите» других делать так же. Да еще и поощряете «отлично выглядит».

                Если вдруг кому-то не очевидно, что плохо:
                1. Нестандартное поведение.
                2. Не будет работать свайп пальцем для возврата (особенно актуально для новых больших экранов).
                3. Кнопка закрытия уезжает в противоположный угол экрана (и нарушение стандартного поведения, и, опять же, забыли про большие экраны).

                  0
                  Предложите ваш вариант, с удовольствием применю его на практике.

                  Если ваше решение — переубедить заказчика, то это не решение поставленной задачи, а её изменение.
                    +2
                    Ну и по пунктам:
                    1. Единственное, что можно принять за недостаток. Тут я да, «прикрылся» волей заказчика. Вот такой я безответственный программист.

                    2. Будет, если не заменять backBarButtonItem своей иконкой.

                    3. То же самое происходит при обычном push segue. Открываешь новое окно кнопкой справа, а закрываешь — кнопкой слева. Как же быть владельцам больших экранов?

                    Напоминаю, что вы критикуете родной, стандартный push segue в iOS. Единственное, что здесь необычно — это показ второго окна при старте приложения, вместо первого.
                    Если модераторы Apple не поймут желания заказчика, анимация будет переделана на modal и нет проблем.

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

                      Сначала показываются все письма и сверху активна кнопка возврата на список к ящиками. Так что, поведение вполне стандартное.
                      +2
                      Вся ваша проблема сводится к решению простой задачи – как сделать так, чтобы navigation controller после инициализации из storyboard уже имел два контроллера в стэке. Ваш вариант решения этой задачи обладает принципиальным недостатками:
                      1. Вы используете -viewDidLoad как место «мгновенного» перехода к следующему контроллеру, хотя это совершенно неподходящее место для выполнения подобных действий – нужно правильно понимать жизненный цикл UIViewController и назначение соотвествующих методов.
                      2. В вашем случае view первого (Info) контроллера всегда загружается, хотя в большинстве сценариев это бесполезно, т.к. пользователь его никогда не увидит – опять же нужно понимать жизненный цикл контроллера, в частности – когда инициализируется его view.
                      3. Вы реализуете логику *навигации* внутри отдельно взятого контроллера – это нарушает принятые в системе уровни абстракции. Представьте, что получится, когда вам потребуется показывать Info контроллер где-то в другой части приложения (или например в iPad-версии) – другим способом и в другом контексте навигации.

                      Есть много вариантов, которые не обладают такими недостатками, а главное – решают задачу на правильном уровне абстракции. В простом варианте можно реализовать соотвествующую логику в AppDelegate:
                      - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
                      	
                          UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
                          UIStoryboard *storyboard = [navigationController storyboard];
                          UIViewController *mainController = [storyboard instantiateViewControllerWithIdentifier:@"идентификатор контроллера из storyboard"];
                          navigationController.viewControllers = [navigationController.viewControllers arrayByAddingObject:mainController];
                          
                          return YES;
                      }
                      
                        0
                        Ну и забыл добавить, что использовать navigation controller для решения такой задачи навигации – это конечно «костыль». И пусть сейчас этот костыль может быть заметен только наметанному взгляду, никто не дает гарантии, что в одной из будущих версий что-нибудь (анимация перехода, взаимодействие с жестами, анимация navigation bar) не изменится так, что результат будет резать глаз даже неискушенному пользователю.

                        В iOS7 и iOS8 появилось несклько API, которые как раз подходят для решения подобных задач: View Controller Transitions, UIPresentationController
                        +1
                        Делал такое три года назад, тогда без сторибоарда еще, через ручное манипулирование стеком NavController'a — по action'у добавлял в начало массива контроллеров нужный и делал `-popViewController...` Всё. Это было в детской книжке и выглядело красиво — как будто одна большая сцена скроллилась…
                          0
                          Я так понимаю, что у модераторов Apple вопросов не возникло?

                          Это радует!
                            0
                            Нет, не вижу причины даже. Интерфейс не нативный, сильно стилизованный (хотя чистый UIKit).

                        Only users with full accounts can post comments. Log in, please.