Предметом исследования будет навигационный контроллер, а именно класс UINavigationController из стандартного фреймворка UIKit для работы с интерфейсом, который нам любезно предоставляет Apple.
Вкратце о...
«Контроллер» в данном случае — некий класс, инкапсулирующий логику, согласно концепции (еще называемой паттерном) MVC.


Навигационный контроллер (UINavigationController) — класс высокого уровня абстракции, содержит в себе иерархию других контроллеров представлений, между представлениями(вьюшками/UIView) которых способен осуществлять навигацию (в чем его, собственно, основная задача и состоит!), передавая в нужный момент управление соответствующему контроллеру. Кроме этого — композиционно содержит в себе навигационную панель (UINavigationBar), которую отображает на экране, и соответствующим образом меняет содержимое данной панели: в зависимости от активного контроллера.
В любой момент из активного контроллера можно получить, как текущий navigation Item, так и navigation Bar:
self.navigationItemself.navigationController.navigationBarИерархическая структура — всегда древовидная:



Предыстория
Мое знакомство с этим элементом управления поначалу было поверхностным, но после одного случая пришлось углубиться. Дело в том, что в одном моем приложении, в разных местах, в связи с большим количеством асинхронности, неслабой связностью — происходило куча всякого непотребства при переходах от одного экрана к другому, да и постоянно происходили двойные переходы, при быстрых касаниях (тачах). До этого мне удавалось успешно справляться различными обходными путями, но куда уж мы бы делись без стремления к совершенному…
В один прекрасный момент мне нужно было прекратить двойной переход (через 2 уровня иерархии, после срабатывания кнопки назад, и быстрого срабатывания). Собственно требовалось поставить блокировку в момент срабатывания перехода. После некоторого исследования выяснилось, что существует 2 способа это сделать:
1) Создать кнопку программно, повесить на навигейшен бар, прикрепить к ней соответствующий селектор (метод-обработчик), в котором явно осуществлять блокировку и вызывать один из методов, по типу popViewControllerAnimated:;
2) Использовать протокол, реализующий делегата для навигационной панели UINavigationBarDelegate.
К сожалению, у первого подхода был явный недостаток: программно создавая кнопку и вешая ее на навигейшен бар, я не смог бы добиться легко стандартной стрелочки и кнопки назад (у меня просто не было этой иконки, она походу берется из стандартных asset-ов (наборов)).
После некоторых проб выяснилось, что UINavigationBarDelegate позволяет, чтобы в качестве делегата был только UINavigationController, и я решился попробовать все-таки сделать подкласс для этого зверя.
О делегировании, навигации и защитном программировании, UINavigationControllerDelegate/UInavigationBarDelegate
Делегирование — один из фундаментальных паттернов проектирования, суть которого в том, что мы делегируем (переназначаем) ответствие за какие-либо действия на класс делегата. Конкретно для objective-c:
Класс делегирующий поведение -> класс-делегат
— назначаем соответствующий протокол классу-делегату, например — определяем все методы со спецификатором @required
и некоторые методы, помеченные ключевым словом @optional
— назначаем классу, который делегирует поведение, этот делегат через свойство делегата (у класса делегирующего должно быть свойство, что-то вроде @property (assign, nonatomic) id delegate;)
— после этого, если мы пишем первый класс, то в нужных местах тягаем методы, не забывая делать проверки по типу
if(self.delegate && [self.delegate conformsToProtocol:@protocol(MyProtocol)] && [self.delegate respondsToSelector: @selector(aMethod)]){ [delegate aMethod]; }
В общем, чем это похоже на то, что один объект нанимает другой объект, чтобы этот объект объяснил ему, что делать и как поступать в определенных ситуациях. Так-то…

Создание нового подкласса на objective-c любят обзывать «субклассированием», поэтому не буду сильно отходить от этих канонов.
В чем преимущество создания подкласса? Сначала я думал обработать в навигейшене только одну ситуацию, но после пришел к выводу, что значительно лучше централизованно обрабатывать все схожие ситуации, внедрить определенные куски кода напрямую в навигейшен, чтобы избавиться от некоторых проблем на корню и для всех других ситуаций. Еще одно преимущество в том, что можно централизованно (в одном месте кода) писать конфигурационный код, который будет общим для каждого контроллера (например, в моем случае — отключать мультитач)
Почти все методы навигации в данном случае начинаются с приставок push/pop, что-то вроде протолкнуть/вытолкнуть (не как в Git-e антонимы push/pull), но такова была принятая не мной конвенция именования целевых методов. Пару слов про UINavigationBar. Он содержит в себе схожую иерархию, но NavigationItem-ов. Эти Item-ы представляют из себя элементы UINavigationBar-a (к сабвьюшкам этого бара, напрямую, доступа нет. Да и в документации явно не рекомендуется каким-либо образом их доставать/менять
frame/bounds/alpha
UINavigationBar-a (он все-таки наследуется от UIView)). То есть конфигурировать навигейшен бар все-таки следует напрямую созданными и инициализированными navigationItem-ами, а все остальное — от лукавого. К чему все это? А к тому, что UINavigationBarDelegate предоставляет доступ к 4-м методам: - (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPushItem:(UINavigationItem *)item; - (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item; - (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item; - (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item;
Только из названия уже должно быть предельно ясно, что это методы по типу will/did. Первый вызывается перед соответствующим действием, второй — после. Только в данном случае первый метод по типу should, еще и являет ответ на вопрос: «выполнять ли это действие?» Таким образом, метод should запускается перед анимацией замены item-a navigationBar-a, а метод did — после. Исходя из задачи, первой моей идеей было блокировать пользовательское взаимодействие в методе should, и возвращать в методе did. Методы push означают движение вниз по иерархии (к более частному), а методы pop — в направлении к корневому.
Одна из ключевых концепций защитного программирования при асинхронности — «обрабатываем соответствующим образом, или блокируем промежуточные состояния». Промежуточные состояния (intermediate states) всегда являются одним из главных источников багов в программах. Так как анимация по своей сути — действие асинхронное (то есть неизвестен точный момент времени, когда вызовется кусок кода, означающий окончание действия, вследствие чего его невозможно синхронизировать с другими кусками кода. Асинхронный код всегда выполняется в отдельном потоке), то его следует экранировать!
По защитному программированию теоретическая часть вполне себе неплохо описана в известном чтиве «Совершенный код»

Кроме того, анимация перехода (segue) с одного корневого представления к другому тоже занимает определенное время, как выяснилось, оно отлично от времени анимации навигационной панели. Длительность анимации UINavigationBar-a статична и определяется константой
extern const CGFloat UINavigationControllerHideShowBarDuration;А длительность анимации перехода может быть различна. Основная причина этого — методы viewDidLoad/viewWillAppear:/методы построения макета (layout-a) по правилам построения (ограничениям/constraint-ам). Соответственно, анимацию перехода — тоже нужно экранировать.
У UINavigationController-a есть протокол делегата UINavigationControllerDelegate. Он определяет 6 методов, 4 связанных с transition-ами, позволяющими обрабатывать непосредственно текущую анимацию (но Available ios 7.0 + соответственно говорит, что они еще недостаточно актуальны), а вот остальные 2 — просто кладезь).
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated; - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
Соответственно обработчики начала и окончания анимации появления контроллера представления.
О переходах (Segues)
Хотелось бы еще пару слов о переходах (segue), в последнее время они стали удобной и модной технологией, так как позволяют на сторибоарде творить чудеса. Ранее для выполнения перехода требовалось инстанцировать экземпляр контроллера, передать нужные данные в объект, и запустить метод pushViewController:animated:, теперь достаточно создать «сегу» на сторибоарде, на экшен, если требуется — повесить идентификатор, конфигурировать. В нашем случае segue navigation controller-a всегда запускаются как push (не как modal или что-то другое).
После этого с любым переходом можно работать в коде, существует 3 метода UIViewController-a:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender; - (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender; - (void)performSegueWithIdentifier:(NSString *)identifier sender:(id)sender;
Первый метод позволяет перед переходом выполнять какие-либо действия с контроллером назначения перед его появлением, обрабатывать различные переходы (
identifier перехода и destinationViewController).Второй метод позволяет, кроме прочего, позволить или прервать выполнение перехода.
Третий метод позволяет программно вызвать переход в коде, собственно он содержит в себе код перехода с
pushViewController:animated:.Самое главное здесь то, что переходы push с помощью segue вызывают одни и те же методы из navigationController-a (если он есть):

Что еще может быть интересно здесь? Существуют так называемые обратные переходы (unwind segue), которые выполняют переходы обратно по контроллерам (они также содержат в себе методы pop). И у каждого из UIStoryboardSegue есть метод perform, в котором можно переопределять анимацию перехода с помощью субклассирования UIStoryboardSegue.
Использование переходов (segue) является наиболее современной практикой выполнения перемещения с одного контроллера представления к другому.
О target-action модели, о взаимодействии пользователя (User Interaction)


И еще для того, чтобы грамотно выполнить поставленную задачу — пару слов о пользовательском взаимодействии с интерфейсом. Когда пользователь касается экрана, генерится и вбрасывается touch event, к сожалению UIEvent не имеет открытого конструктора, так что мы не имеем возможности легко создавать наши события касания к экрану устройства, таким образом эмулируя данную ситуацию. Контролы во всем приложении реагируют на соответствующие события (event-ы), им предназначенные, в результате чего интерфейс становится интерактивным и реагирующим на действия пользователя.
Некоторые действия на события уже предопределены (например, когда мы делаем touch down по кнопке — кнопка переходит в состояние highlighted (подсвечена), и меняет внешний вид). Мы можем перехватывать события, и обрабатывать их, как нам вздумается, назначая обработчики, через селекторы. Селектор хранит в себе хэш-значение, позволяющее быстро выбрать связанный с ним метод из хэш-таблицы селекторов класса. Все Event-ы назначаются и направляются (если не ошибаюсь) в недрах класса UIApplication, который имеет 2 важных метода
- (void)sendEvent:(UIEvent *)event; - (BOOL)sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event;
В общем, это реализация target-action паттерна:

Существует 2 способа блокировать пользовательское взаимодействие: первый — блокирование получения событий конкретным элементом управления (контролом); второй — блокирование отправки событий непосредственно из объекта-экземпляра приложения.
1й способ (у каждого View есть свойство userInteractionEnabled):
self.navigationController.navigationBar.userInteractionEnabled = NO; self.someButton.userInteractionEnabled = YES;
2й способ (объект приложения является синглтоном):
[[UIApplication sharedApplication] beginIgnoringInteractionEvents]; [[UIApplication sharedApplication] endIgnoringInteractionEvents];
Так как имеется нужда блокировать любое взаимодействие (неизвестно при нажатии конкретно по какой кнопке будет выполняться опасный код (со следующим далее переходом)), то нам подходит второй способ.
Внешний вид navigation-bar-a
Как вы, может, знаете, наилучшая практика — это задавать внешний вид с помощью UIAppearance, но благодаря подобному подклассу, можно и отказаться от нее, если использовать везде этот подкласс. К тому-же инкапсулировать эту логику (сокрыть) внутри навигейшен контроллера является весьма грамотным решением. Для этого подходит метод
awakeFromNib. Самолично я не пытался делать такое, но подсмотрел у других. Это был небольшой совет. Мультитач
Если кому-то интересно про мультитач (чтобы не было возможности нажать подряд 2 кнопки):
- (void) makeExclusiveTouchToSubviews:(UIView*)view { for (UIView * currentSubtView in [view subviews]) { currentSubView.multipleTouchEnabled = NO; currentSubView.exclusiveTouch = YES; [self makeExclusiveTouchToSubviews:currentSubView]; } }
PS. если вы хотите воспользоваться сиим чудом, пользуйтесь на свой страх и риск, я далеко не все опробовал из того, что имелось, так что для некоторых ситуаций вам придется, возможно, дописывать самим. Классы Utility/GAIClient не поставляются (из первого берется метод на отключение мультитача, с помощью второго — отсылается non-crash репорт на GoogleAnalytics).
Реализованный функционал
Было реализовано:
- Способ блокировать переходы быстро вручную (в случае надобности);
- 3 уровня защиты от переходов:
а) на уровне методов should navigationBarDelegate;
в) соответственно блокированием пользовательского взаимодействия, если началась хотя-бы одна соответствующая анимация, и разблокированием, если завершились все;
вшитая защита от обработки экшенов сразу 2х кнопок (посредством отключения мультитача);
механизм деблокирования, в случае если что-то пошло не так;
создание репорта, если что-то пошло не так.
Возникшие нюансы и проблемы
1-я проблема была связана с тем, что при использовании явного и неявного переходов (во втором случае через navigation bar-кнопку «Back») во втором случае не запускается метод
popToViewController:animated:, пришлось явно проверять, осуществляется ли уже переход с одного контроллера на другой;2-я проблема — поведение navigation-bar-a на iOS 7.0. На этой прошивке для стандартного навигейшен контроллера делегат назначается автоматически (и если мы еще раз пытаемся это сделать вручную — генерит исключение (exception)).
3-я проблема — на 7й прошивке имеется правый свайп interactivePopGestureRecognizer, который позволяет делать переходы назад (он вызывал только метод navigation controller delegate will, из-за чего намертво блочил пользовательское взаимодействие).
4-я проблема — в крайне редких ситуациях могла возникнуть опасность, что не всегда запускался противоположный метод (система должна была быть в случае чего самовосстанавливающейся). Было реализовано подобие таймера, с обработчиком-деблокиратором.
Скачать/посмотреть
Git Repo на GitHub-e

