Как стать автором
Обновить

Комментарии 28

Наблюдал ещё одну подобную интересную ситуацию: если во время анимации перехода на новый экран push-нуть ещё один новый экран… Симулятор просто выдаст ошибку в лог, мол, не могу. И всё. Никаких крэшей, падений… никаких проблем. Просто перешел на экран, на который переходил изначально, а не на следующий. Получалось это, когда переход помещал в ViewDidLoad. Решение — вынести код в например, ViewDidAppear.
А что если перед пушем отключать userInteraction на текущем скрине?
Есть у вас готовый тестовый проект, на котором можно добиться краша?
Мы одумали про userInteraction, но в случае двойного нажатия это не спасает.
Готового проекта нет, но его совсем не трудно сделать самостоятельно. Первый пример кода показывает как это сделать.
Сделал тестовый проект, вот ссылка
Да, спасибо, уже поигрался.
Тем временем мне в скайп постучался товарищ ASkvortsov:
Привет, хочу поделиться по поводу статьи с хабра

у меня просто r/o акк, поэтому не могу откомментить

developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/occ/instp/UIView/exclusiveTouch — то, что нужно использовать

нужно проставить его в YES обоим кнопкам


И этот метод работает.
Видимо нам нужно учить матчасть :)
Любопытная штука. Спасибо. Плюсую. Правда, не соглашусь с тем, что ситуация «самостоятельно скажи каждому контролу, что его нельзя нажимать параллельно с остальными» — это не корявость системы и костыль, а «важная фича, которую нужно откопать в документации и запомнить».
Если выбирать из двух решений (кастомный навигейшн или метод контрола), то я за стандартный ;)
С этим не спорю.
А я — за кастомный. Потому что его надо один раз сделать — и он становится твоим стандартом, а про «стандартный» надо помнить всякий раз, когда в проект добавляется новая кнопка.
Ваше право. Я предпочитаю стандартный, потому что его проще поддерживать. Да и предложенный вариант выглядит костыльно, потому как вряд ли пользователь желает открыть два контроллера сразу.
Не то чтобы он выглядит костыльно, скорее странно. Я бы лучше сделал не очередь переходов, а запрет перехода во время перехода — потому что пользователь и правда два контроллера сразу не открывает, это — ситуация-ошибка с неопределенным ожидаемым поведением.
Сделайте себе кастомную кнопку, это намного проще и лучше кастомного навигейшен контроллера.
Проблема множественных нажатий не ограничивается навигейшен контроллером как в статье. Серчбар + пуш, поповер + пуш, модальное окно + поповер, поповер + поп, и т.д. Все сильно зависит от дизайна и архитектуры приложения, во многих случаях, такие действия к крешу не приведут, но могут вылезти различные UI баги. Поэтому, все кнопки, ячейки, барбаттонитемы которые как-то меняют не данные, а представления на экране, должны иметь exclusiveTouch, это самая простая и железная защита от багов такого рода.
mshershnev Предлагаю добавить это решение в конец поста :)
Вы серьёзно считаете, что из-за button.exclusiveTouch = NO стоит писать целую статью? Это всё тянулось как минимум с iOS 6, где можно было развлекаться, роняя системные приложения подобным образом. Другой вопрос, почему Apple решили, что UButton должен по умолчанию иметь .exclusiveTouch = NO, и почему не исправляли это так долго.

Ну и вы лукавите, что «В сети описание этой ошибки встречается очень редко, а решение было найдено всего одно, и оно не работает»: раз, два (аж 2012 год).
При чем здесь UIButton? Запустить пуши можно кучей способов, а маскировать таким образом ошибку в UINavigationController не лучшая практика.
При том, что если вы стремитесь выстрелить себе в ногу, пуша два контроллера одновременно и анимировано, это полностью ваша проблема, а не «ошибка» UINavigationController. Если нужно запушить в стек два и более контроллера, правильнее сделать так:

UIViewController* firstViewController = [[UIViewController alloc] init];
UIViewController* secondViewController = [[UIViewController alloc] init];
UIViewController* thirdViewController = [[UIViewController alloc] init];

[self.navigationController pushViewController:firstViewController animated:NO];
[self.navigationController pushViewController:secondViewController animated:NO];
[self.navigationController pushViewController:thirdViewController animated:YES];

[firstViewController release];
[secondViewController release];
[thirdViewController release];

Хотя я не могу представить сценария, где надо пушить контроллеры одновременно: пуш должен быть инициирован пользователем, и сразу после пуша пользователь должен иметь возможность вернуться назад, совершив ровно одно действие. Если же действие пользователя инициирует двойной, тройной и т.д. пуш, пользователь слегка удивится тому, что кнопка «назад» возвращает его не на тот контроллер, из которого он сюда попал.
Ну не все используют эту технологию. :)
Вам плюс за попытку улучшить мир. Однако практическая польза от этого, к сожалению, никакая. Написание своей обертки для UINavigationController ради такой экзотической ошибки неоправданно. Я имею ввиду, что небольшая вероятность внести собственную (другую) ошибку в свой код оказывается выше (на мой субъективный взгляд) микроскопической вероятности, что пользователь наткнется на описанную проблему.
«Непонятно, почему Apple не считает это проблемой и не занимается её решением.» — думаю примерно по той же причине. К тому же ошибок такого низкого класса я думаю в iOS не одна и не две. Просто это та, которую вы заметили. В баг-трекере разработчика любого сколько-нибудь серьезного софта копятся такие ошибки. И если они не приводят к уязвимостям в системе их очень редко исправляют.
Первое, что я говорю нашим тестировщикам, это тестить такие вот кейсы с множественными нажатиями. Тот мегакостыль, который вы тут описали просто взорвал мне мозг. И то, он полностью не решает проблему двойных нажатий, только внутри навигейшен контроллера. А если у вас две кнопки фильтруют/меняют датасорс таблички, одновременное их нажатие, скорее всего, убъет контроллер. И для этого, у UIView есть пропертя exclusiveTouch, плохо, что она по дефолту не YES. И ее нельзя поставить глобально через UIAppearance, например.

В общем, горе от ума. Плохо, что большинство iOS разработчиков не читают документацию, а предпочитают StackOverflow Driven Development.
чаще всего, если ответ есть в документации, то на SO просто дают ссылку :)
Где-нибудь пятым ответом, а на первом месте почти всегда заплюсованный говнокод, который продолжают плюсовать по инерции.
Я предпочитаю проверять перед вызовом pushViewController а не равен ли topViewController текущему, и если вдруг нет то и пушить не надо.
Ровно такая же проблема существует при использовании сторибордов. Если есть сегвей на новую сцену и в этой сцене что-то внезапно долго будет выполняться (конструктор, didload, ватевер) и тут же вызвать другой сегвей — будет ровно такая же история.
Но тут, к сожалению, своим NavigationController не обойдёшься, поэтому тут рекомендация одна — не выполнять потенциально длинный код в основной очереди
Кроме описанного коллегами до меня, у вас еще и тут опасненькая ситуация:
@property (nonatomic, strong) NSMutableArray *tasks;
           
void (^task)(void) = ^{
       [self pushViewController:viewController animated:animated];
};
            
[self.tasks addObject:task];


Пока не будут завершены все ваши анимации, навконтроллер не умрет, что не очень-то хорошо. Такой себе локальный ретейн луп.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий