Исторически сложилось так, что одно из различий, из-за которого гибридные мобильные приложения кажутся немного выключенными, заключалось в том, что при обработке нажатий на элементы пользовательского интерфейса с помощью простого click обработчика событий возникала задержка. Библиотеки, такие как Fastclick, были созданы для смягчения этого за счет использования необработанных событий касания для немедленного запуска обработчиков событий. Хотя они работали для базового использования, они добавляли накладные расходы на выполнение JavaScript для событий касания, что приводило к мусору.

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

Я предположил, что это применимо к веб-представлениям, встроенным в приложения, но я был разочарован, увидев, что поведение Quip не улучшилось в iOS 9.3 или 10.0 (у нас есть собственный Fastclick-подобный оболочка для большинства обработчиков событий, но она не применялась к флажкам, и те продолжали отставать). Еще несколько исследований показали, что улучшение не применимо к UIWebView (старый механизм встраивания веб-представлений в приложения для iOS - WKWebView более современный, но все еще имеет некоторые ограничения, и поэтому Quip не перешел на него).

Сообщение в блоге WebKit об улучшениях включало некоторые ссылки на связанные отслеживающие ошибки (как уже упоминалось ранее, WKWebView является полностью открытым исходным кодом, что по-прежнему приятно). Копаясь в одном из связанных коммитов, похоже, что это был вопрос настройки взаимодействия между несколькими UIGestureRecognizer экземплярами. Обычно тот, который обрабатывает одиночные нажатия, должен дождаться отказа того, который обрабатывает двойные нажатия, прежде чем запускать свое действие. Поскольку двойное нажатие занимает 350 миллисекунд, чтобы определить, следует ли за одним касанием другое, именно столько времени требуется для отказа для одиночных нажатий. Apple изменила этот второй распознаватель жестов для немасштабируемых страниц.

UIWebView не является открытым исходным кодом, но я решил, что его реализация должна быть аналогичной. Чтобы проверить это, я добавил небольшой фрагмент кода, чтобы выгрузить все распознаватели жестов для его иерархии представлений (запускается с помощью [self dumpGestureRecognizers:uiWebView level:0]:

-(void)dumpGestureRecognizers:(UIView *)view level:(int)level {
    NSMutableString *prefix = [NSMutableString new];
    for (int i = 0; i < level; i++) {
        [prefix appendString:@"  "];
    }
    NSLog(@"%@ view: %@", prefix, view);
    if (view.gestureRecognizers.count) {
        NSLog(@"%@ gestureRecognizers", prefix);
        for (UIGestureRecognizer *gestureRecognizer in view.gestureRecognizers) {
            NSLog(@"%@   %@", prefix, gestureRecognizer);
        }
    }
    for (UIView *subview in view.subviews) {
        [self dumpGestureRecognizers:subview level:level + 1];
    }
}

Это показало, что UIWebView содержит UIScrollView, который, в свою очередь, содержит UIWebBrowserView. В этом представлении есть несколько распознавателей жестов, наиболее интересным из которых является UITapGestureRecognizer, который требует одного касания и касания и имеет в качестве действия _singleTapRecognized селектор. Разумеется, для этого требуется сбой другого распознавателя жестов, который принимает два касания (для него задано действие _doubleTapRecognized, что дополнительно проясняет его цель).

<UITapGestureRecognizer: 0x6180001a72a0; 
    state = Possible; 
    view = <UIWebBrowserView 0x7f844a00aa00>; 
    target= <(action=_singleTapRecognized:, target=<UIWebBrowserView 0x7f844a00aa00>)>; 
    must-fail = {
        <UITapGestureRecognizer: 0x6180001a7d20; 
            state = Possible; 
            view = <UIWebBrowserView 0x7f844a00aa00>; 
            target= <(action=_doubleTapRecognized:, target=<UIWebBrowserView 0x7f844a00aa00>)>; 
            numberOfTapsRequired = 2>,
        <UITapGestureRecognizer: 0x6180001a8180; 
            state = Possible; 
            view = <UIWebBrowserView 0x7f844a00aa00>; 
            target= <(action=_twoFingerDoubleTapRecognized:, target=<UIWebBrowserView 0x7f844a00aa00>)>; 
            numberOfTapsRequired = 2; numberOfTouchesRequired = 2>
    }>

Затем в качестве эксперимента я добавил фрагмент, чтобы отключить этот распознаватель двойного касания:

for (UIView* view in webView.scrollView.subviews) {
    if ([view.class.description equalsString:@"UIWebBrowserView"]) {
        for (UIGestureRecognizer *gestureRecognizer in view.gestureRecognizers) {
            if ([gestureRecognizer isKindOfClass:UITapGestureRecognizer.class]) {
                UITapGestureRecognizer *tapRecognizer = (UITapGestureRecognizer *) gestureRecognizer;
                if (tapRecognizer.numberOfTapsRequired == 2 && tapRecognizer.numberOfTouchesRequired == 1) {
                    tapRecognizer.enabled = NO;
                    break;
                }
            }
        }
        break;
    }
}

Как только я это сделал, были немедленно отправлены click событий с минимальной задержкой. Я создал простую тестовую площадку, которая показывает разницу между обычным UIWebView, WKWebView и взломанным UIWebView с помощью распознавателя жестов. Хотя WKWebView все еще на пару миллисекунд быстрее, дела обстоят намного лучше.

Обратите внимание, что UIWebBrowserView является частным классом, поэтому наличие ссылки на него может привести к отклонению App Store. Вы можете поискать альтернативные способы обнаружения распознавателя жестов. Quip работает с этим хаком уже пару месяцев без каких-либо побочных эффектов. Я сожалею только о том, что я не подумал об этом раньше, у нас (и других гибридных приложений) могли быть клики без задержек в течение многих лет.

Изначально это сообщение появилось на persistent.info.