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

Предметом исследования будет навигационный контроллер, а именно класс UINavigationController из стандартного фреймворка UIKit для работы с интерфейсом, который нам любезно предоставляет Apple.

Вкратце о...


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





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

В любой момент из активного контроллера можно получить, как текущий navigation Item, так и navigation Bar: self.navigationItem
self.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

Листинги кода:
HUNavigationController.h
//
//  HUNavigationController.m
//
//  Created by HuktoDev on 03.07.15.
//


#import <UIKit/UIKit.h>

/* Подкласс NavigationController-a, предоставляет механизм централизованной защиты всех контроллеров от двойных переходов, и от мультитача.
   Механизм защиты от переходов реализован, как полное блокирование пользовательского взаимодействия (event-ов в приложении) во время переходных состояний, ютаких как
  а) анимации navigation-бара
  б) анимированных переходов между представлениями
   
   На случай не срабатывания деблокирования - аккуратно вшит механизм раблокирования экрана по таймеру после блокировки
 
   если произойдет длительная блокировка - отсылает репорт в гугл аналитикс*/


#warning Сделать свой особый тип логов для навигейшена

@interface HUNavigationController : UINavigationController <UINavigationBarDelegate, UINavigationControllerDelegate, UIGestureRecognizerDelegate>

@property (assign, nonatomic) BOOL isBarPopProcessing;
@property (assign, nonatomic) BOOL isBarPushProcessing;
@property (assign, nonatomic) BOOL isTransitionControllerProcessing;

@property (assign, nonatomic) BOOL isBlockedInteraction;

-(void)blockAllInteraction;
-(BOOL)restoreAllInteraction;
-(void)makeExclusiveTouchToViewController:(UIViewController*)viewController;

/* метод для проверки, возможно ли обработать кастомный пуш/поп*/
-(BOOL)isNeedNavigationBarActionBlocking;

-(BOOL)isInteractionDisabled;


@end



HUNavigationController.m
//
//  HUNavigationController.m
//
//  Created by HuktoDev on 03.07.15.
//

#import "HUNavigationController.h"

@implementation HUNavigationController{
    NSTimer *timerCheckBlocking;
    NSDate *dateStartBlocking;
}

#pragma mark - UIViewController cycle

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //инициализация булевых флагов
    self.isBarPopProcessing = NO;
    self.isBarPushProcessing = NO;
    self.isTransitionControllerProcessing = NO;
    
    self.isBlockedInteraction = NO;
    
    //назначение делегатов
    self.delegate = self;
    
    //на 7й прошивке - делегат назначается автоматически, иначе эксепшен
    if(!self.navigationBar.delegate){
        self.navigationBar.delegate = self;
    }
    
    //разлочить все, что нужно
    self.navigationBar.userInteractionEnabled = YES;
    
    [self endIgnoringIf:[self isInteractionDisabled]];
    
    //запустить таймер проверки блокировки
    timerCheckBlocking = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(checkBlocking) userInfo:nil repeats:YES];
}

/* в iOS 7 - используется правый свайп для возврата к предыдущему контроллеру (блокирование подобного поведения */
-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]){
        self.interactivePopGestureRecognizer.enabled = NO;
        self.interactivePopGestureRecognizer.delegate = self;
    }
}

-(void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]){
        self.interactivePopGestureRecognizer.enabled = YES;
        self.interactivePopGestureRecognizer.delegate = nil;
    }
}

-(void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];
    
    //при сокрытии навигейшена - попытаться на всякий случай снять блокировку и отменить таймер
    [self endIgnoringIf:[self isInteractionDisabled] ];
}

#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    return NO;
}

/*переопределение методов стандартного navigation-a (один из уровней защиты от нежелательных переходов) */
#pragma mark - UINavigationController segues methods wrappers -

-(UIViewController *)popViewControllerAnimated:(BOOL)animated{
    if([self isNeedNavigationBarActionBlocking]){
        return nil;
    }else{
        return [super popViewControllerAnimated:animated];
    }
}

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated{
    if([self isNeedNavigationBarActionBlocking]){
        return [NSArray array];
    }else{
        return [super popToViewController:viewController animated:animated];
    }
}

-(NSArray *)popToRootViewControllerAnimated:(BOOL)animated{
    if([self isNeedNavigationBarActionBlocking]){
        return [NSArray array];
    }else{
        return [super popToRootViewControllerAnimated:animated];
    }
}

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
    if(![self isNeedNavigationBarActionBlocking]){
        [super pushViewController:viewController animated:animated];
    }
}


#pragma mark - UINavigationBarDelegate

-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item{
    
    //только для кнопок back bar button (автоматический переход)
    //если кастомный баттон - то переход уже начинает идти, и тогда нужно проверить, и вернуть YES
    
    //блокируем множественные вызовы методов делегата navigation bar-a
    if(self.isBarPopProcessing || self.isBarPushProcessing){
        return NO;
    }
    
    self.isBarPopProcessing = YES;
    
    //для переходов по-умолчанию (например с помощью back) (у тех, у кого самостоятельно не запускается popViewControllerAnimated, соответственно еще не заблочены интерэкшены
    if(! self.isTransitionControllerProcessing && ! self.isBlockedInteraction){
        [super popViewControllerAnimated:YES];
    }
    
    [self blockAllInteraction];
    return YES;
}

/* метод окончания анимации айтема бара (попытаться разлочить взаимодействие)*/
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item{
    self.isBarPopProcessing = NO;
    [self restoreAllInteraction];
}

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPushItem:(UINavigationItem *)item{
    
    //если анимация бара еще идет - не совершать действие (для кастомных кнопок, выполняющих segue/push - предварительно всегда в коде контроллера должна стоять проверка
    //защита от множественных вызовов метода
    if(self.isBarPopProcessing || self.isBarPushProcessing){
        return NO;
    }
    
    self.isBarPushProcessing = YES;
    [self blockAllInteraction];
    return YES;
}

/* метод окончания анимации айтема бара (попытаться разлочить взаимодействие)*/
- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item{
    
    self.isBarPushProcessing = NO;
    [self restoreAllInteraction];
}

#pragma mark - UINavigationControllerDelegate

/* начало анимации любого контроллера иерархии, и окончание анимации. Отключение мультитача при каждом появлении контроллера. Аналогично блокировка при начале анимации, разблокировка при окончании*/

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
    NSLog(@"WILL SHOW");
    self.isTransitionControllerProcessing = YES;
    
    //место, где можно централизованно вызывать общий для каждого контроллера код инициализации (общий viewWillAppear:)
    [self makeExclusiveTouchToViewController:viewController];
    [self blockAllInteraction];
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
    
    NSLog(@"DID SHOW");
    //место, где можно централизованно вызывать общий для каждого контроллера код инициализации (общий viewDidAppear:)
    
    self.isTransitionControllerProcessing = NO;
    [self restoreAllInteraction];
}

#pragma mark - User Interaction manage

/* блокиратор/деблокиратор
  */

-(void)blockAllInteraction{
    NSLog(@"TRY TO BLOCK ALL INTERACTION");
    
    //если еще не заблокировано взаимодействие - отменить предыдущий таймер разблокировки, заблокировать, и запустить новый 2х-секундный таймер на деблокировку
    
    if(! [self isInteractionDisabled]){
        
        NSLog(@"ATTEMPT SUCCESS BLOCK INTERACTION");
        
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(restoreAllInteraction) object:nil];
        [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
        self.isBlockedInteraction = YES;
         [self checkBlocking];
        [self performSelector:@selector(restoreAllInteraction) withObject:nil afterDelay:2.f];
    }
}

-(BOOL)restoreAllInteraction{
    NSLog(@"TRY TO RESTORE ALL INTERACTION");
    
    //отменить предыдущий реквест на восстановление, попытаться восстановить взаимодействие, если никакая анимация более не идет и хоть что-либо является заблокированным
    //если не удается - запустить таймер на будущее на восстановление (будет рекурсивно запускаться, пока не выполнятся условия
    
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(restoreAllInteraction) object:nil];
    
    BOOL isRestoreSuccess = [self endIgnoringIf:(! self.isBarPopProcessing && ! self.isBarPushProcessing && [self isInteractionDisabled] && ! self.isTransitionControllerProcessing)];
    if(isRestoreSuccess){
        NSLog(@"ATTEMPT SUCCESS RESTORE INTERACTION");
         [self checkBlocking];
        return YES;
    }else{
        if([self isInteractionDisabled] ){
            [self performSelector:@selector(restoreAllInteraction) withObject:nil afterDelay:2.f];
        }
        return NO;
    }
}

/* кондишены, 1) публичный, для проверки того, можно ли выполнять переход в коде контроллера*/
-(BOOL)isNeedNavigationBarActionBlocking{
    return (self.isBarPopProcessing || self.isBarPushProcessing || self.isTransitionControllerProcessing);
}

/* метод, основная точка доступа к информации о текущем состоянии пользовательского взаимодействия*/
-(BOOL)isInteractionDisabled{
    return (self.isBlockedInteraction || [UIApplication sharedApplication].isIgnoringInteractionEvents);
}

/* перестаем блокировать user interaction, если условие выполняется*/
-(BOOL)endIgnoringIf:(BOOL)condition{
    if(condition){
        [[UIApplication sharedApplication] endIgnoringInteractionEvents];
        self.isBlockedInteraction = NO;
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(restoreAllInteraction) object:nil];
        return YES;
    }else{
        return NO;
    }
}

#pragma mark - Multitouch block

/* метод отключения мультитача*/

-(void)makeExclusiveTouchToViewController:(UIViewController*)viewController{
    [Utility makeExclusiveTouchToSubviews:viewController.view];
}

#pragma mark - Google analytics reports
/* постоянно проверять, если интервал блокировки длится более 10 секунд - отослать репорт в гугл аналитикс*/

-(void)checkBlocking{
    
    if(!dateStartBlocking && [self isBlockedInteraction]){
        dateStartBlocking = [NSDate date];
    }else if(dateStartBlocking && ! [self isBlockedInteraction]){
        NSLog(@"interface was blocked on %.1f seconds", -([dateStartBlocking timeIntervalSinceNow]));
        dateStartBlocking = nil;
    }else if(dateStartBlocking && [self isBlockedInteraction]){
        NSTimeInterval intervalBlocking = [dateStartBlocking timeIntervalSinceNow];
        if(intervalBlocking > 10.f){
            //можно собирать логи, и отправлять логи, а не стектрейсы
            [self sendInteractionBlockingReport];
            if([timerCheckBlocking isValid]){
                [timerCheckBlocking invalidate];
            }
            dateStartBlocking = nil;
        }
    }
}

-(void)sendInteractionBlockingReport{
    NSLog(@"ERROR INTERFACE LOCK !!!");
    NSString *descriptionLockReport = [NSString stringWithFormat:@"controller %@ block interaction \nvars : \nisBarPopProcessing %i \nisBarPushProcessing %i \nisTransitionControllerProcessing %i \nisBlockedInteraction %i", self.visibleViewController, self.isBarPopProcessing, self.isBarPushProcessing, self.isTransitionControllerProcessing, self.isBlockedInteraction];
    [[GAIClient sharedInstance] sendReportNonFailExceptionWithDescription:descriptionLockReport];
}

#pragma mark - Destruction

-(void)dealloc{
    //при очистке памяти - разблокировать, если требуется (критично)
    [self endIgnoringIf: [self isInteractionDisabled]];
    if([timerCheckBlocking isValid]){
        [timerCheckBlocking invalidate];
    }
}

@end


Комментарии (13)


  1. Makaveli
    20.07.2015 14:38
    +2

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


    А в чём проблема?

    self.navigationController.navigationBar.topItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
    


    1. HUktoCode Автор
      20.07.2015 17:39

      вот оплошал, оно со стрелочкой создает)) вроде бы и помню, что когда делал как-то не то вышло, а после уже махнул рукой на попытки — и пошел вторым путем) Может как-то немного не так пытался =) буду теперь в курсе


  1. fiveze
    20.07.2015 23:55

    Почти все методы навигации в данном случае начинаются с приставок push/pop, что-то вроде протолкнуть/вытолкнуть (не как в Git-e антонимы push/pull)

    UINavigationController при работе с контроллерами представляет собой стек, отсюда и приставки push/pop.

    Спасибо, статья хорошая.


  1. corristo
    21.07.2015 01:22

    >на 7й прошивке имеется правый свайп interactivePopGestureRecognizer, который позволяет делать переходы назад (он вызывал только метод navigation controller delegate will, из-за чего намертво блочил пользовательское взаимодействие).

    Отловить «did» в этом случае можно через transitionCoordinator, подписавшись на завершение транзишена, и в коллбэеке проверив что он не был отменен.


    1. HUktoCode Автор
      21.07.2015 20:31

      не пробовал пользоваться) методы на завершение транзишена с 7й прошивки, а у меня текущий проект с ios 6 sdk, к тому-же если будет такое поведение только на устройствах с такой вот прошивкой — это будет не слишком хорошо, так как поведение везде должно быть идентичным. Но интересный способ отлавливания


      1. corristo
        21.07.2015 20:32

        В том-то и суть, как вы сами сказали — на 6 описанной проблемы нет (так как нет interactive transitions), поэтому спокойно можно использовать этот вариант только в code path для iOS 7+.


  1. storoj
    21.07.2015 01:44

    фак мой мозг

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

    Раз уж прям заниматься этим вопросом, то вместо таймеров и retain-циклов предпочел бы private api.

    В статье говорится, что

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

    Но ведь
    NSLog(@"animation begin");
    [CATransaction begin];
    UIViewController *tmp = [UIViewController new];
    tmp.view.backgroundColor = [UIColor redColor];
    [navigationController pushViewController:tmp animated:YES];
    [CATransaction setCompletionBlock:^{
        NSLog(@"animation end");
    }];
    [CATransaction commit];
    


    1. HUktoCode Автор
      21.07.2015 20:36

      разве begin / commit методы здесь служат не для группировки анимаций? При вызове коммита — создается явная транзакция, и на GPU отдельно обрабатывается в отдельном потоке (асинхронно). Разве не?

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


  1. alkozin
    21.07.2015 07:44
    +1

    if(self.delegate && [self.delegate conformsToProtocol:@protocol(MyProtocol)] && [self.delegate respondsToSelector: @selector(aMethod)]){
            [delegate aMethod];
    }
    

    В Obj C не нужно делать проверку на то что объект существует перед тем как послать сообщение.
    В этом случае вернется NO.
    Зачем проверять что объект поддерживает протокол? Лучше объявить его правильно:
    @property (weak, nonatomic) id < MyProtocol> delegate;
    

    Поэтому для optional методов остается так:
    if ([delegate respondsToSelector:@selector(aMethod)]){
            [delegate aMethod];
    }

    Для @required без проверок, метод же и так обязательный:
    [delegate aMethod];
    

    [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
    [[UIApplication sharedApplication] endIgnoringInteractionEvents];
    
    Не делайте так. Если endIgnoringInteractionEvents не вызовется потом будет очень сложно понять в чем баг.
    Я один раз сталкивался с кодом где это сломалось. Было очень плохо.
    Если уж очень надо, выключайте у вью. А лучше все-таки делать проверку по флагу.

    Чтобы заблокировать двойные пуши из-за двойных тапов или асинхронных вызовов я бы сначала попробовал проверять контроллер который должен отобразиться на isEqual: с тем что сейчас видимый или с теми что в стеке если необходимо.
    Эквивалентность определяем по эквивалентности объекта который нужен контроллеру.
    Например для PostViewController это должен быть Post.
    А эквивалентность постов проверяем по uid.

    Пробовали такой подход?


    1. HUktoCode Автор
      21.07.2015 20:44

      1) я привел наиболее общий подход проверки перед вызовом метода
      2) ранее я тоже писал через weak, но в последнее время в большинстве мест у Apple встречаю запись через assign. Тоже, на самом деле, удивило. Но привел здесь вариант со спецификатором assign
      3) если аккуратно использовать метод endIgnoringInteractionEvents — то ничего, а что лочило, пока не довел до ума — такое было. Я не упомянул один недостаток блочить чисто вьюхи. Блокирую таким образом — кнопка back так и останется подсвеченной. Видимо по какой-то причине приходит touchDown, а touchUpInside/touchUpOutside не приходит
      4) нет, не пробовал. По правде, даже и не до конца понял суть данного подхода, как его применить


  1. systemroot
    30.07.2015 11:19

    Поясните, пожалуйста, что вы понимаете под экранированием ассинхронного кода. Спасибо!


    1. HUktoCode Автор
      02.08.2015 13:06

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


    1. HUktoCode Автор
      02.08.2015 13:09

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