Как убрать зависания UIPickerView в симуляторе iOS

Время от времени замечаю, что случаются зависания, когда в симуляторе пытаюсь выбрать элемент в UIPickerView. Но в той степени, в которой тормоза проявляются сейчас, стало невыносимо наблюдать: изменение выбранного элемента в «барабане» может занять до минуты, в течение которой интерфейс ни на что не реагирует.

Возможно, это недоработка бета-версий.
На чистом проекте специально для исследования этой проблемы наблюдается всё точно то же.

Данная проблема проверялась на 4 вариантах запуска:
Xcode 6.4 + 8.1 проявляется
Xcode 6.4 + 8.3 проявляется
Xcode 7.0 + 8.3 проявляется
Xcode 7.0 + 9.0 не проявляется

Наводит на мысль, что имеет место быть какое-то легкое несоответствие работоспособности версий симулятора, которое в данном случае очень сильно напрягает вариантом проявления.

Попробуем устранить проблему.
Запускаем довольно простой проект, в который добавлен «барабан»:

image


И теперь пробуем слегка мотнуть список вверх.
В результате он застывает в такой позиции, в данном случае на 28 секунд:

image


Ну что ж, посмотрим, что заставляет его задуматься.
Ставим на паузу и смотрим backtrace:

image


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

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

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

Примера ради попробуем сотворить нужное во время загрузки приложения.
Например, внутри AppDelegate.

#if TARGET_IPHONE_SIMULATOR
- (void)UIPickerTableView__playClickIfNecessary
{
    // nothing to do
}
#endif


+ (void)initialize
{
#   if TARGET_IPHONE_SIMULATOR
    Class srcClass = self;
    Class dstClass = NSClassFromString(@"UIPickerTableView");
    
    if (srcClass && dstClass) {
        SEL srcSelector = NSSelectorFromString(@"UIPickerTableView__playClickIfNecessary");
        SEL dstSelector = NSSelectorFromString(@"_playClickIfNecessary");
        
        Method srcMethod = class_getInstanceMethod(srcClass, srcSelector);
        Method dstMethod = class_getInstanceMethod(dstClass, dstSelector);
        
        method_exchangeImplementations(srcMethod, dstMethod);
    }
#   endif
}

Пытался сделать замену через категорию, но не получилось:
— через таблицу — потому что её реализацию перекрывает UIPickerTableView;
— через UIPickerTableView, потому что не смог нормально импортировать скрытый API.

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

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

© Habrahabr.ru