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

Синхронная загрузка UIWebView

Время на прочтение4 мин
Количество просмотров6K
Приветствую, Хабр!

Все началось с поиска решения задачи отображения форматированного текста внутри UITableViewCell, причем не строго заданного формата (тогда можно было бы использовать набор UILabel c заданным font) а произвольного. Да так, чтобы форматирование можно было задать простейшими html тегами. Решить такую задачу можно по-разному:
  • Реализовать кастомный компонент с использованием CoreText (не подходит если нужна iOS3.x совместимость)
  • Реализовать кастомный компонент с использованием CoreGraphics (очень объемная работа)
  • Реализовать кастомный компонент с динамическим число UILabels в качестве subviews (довольно мутно в связи с переносами и прочим)
  • UIWebView c загрузкой через loadHTMLString


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

UIWebView inside UITableViewCell


И так, в чем же чуть этой проблемы — UIWebView грузит свой контент асинхронно. И если мы разместим UIWebView внутри UITableViewCell, и в cellForRowAtIndexPath: будем загружать сам контент, фактическая его загрузка будет происходить асинхронно, после того как пройдут все touch события связанные со скроллированием. Выглядеть это будет не очень приятно: контент начнет обновляться только после того как скроллинг остановится.
Чтобы обойти это ограничение следует немного разобраться в теории. Асинхронность в нашем случае реализуется с помощью RunLoops. Так как runloop устанавливается для потока, мы можем выполнять вызов асинхронно в том же потоке. Кроме того, каждый вызов делается со своим RunLoopMode — что является аналогом приоритетов, таким образом в первую очередь из текущего лупа выбираются все вызовы наивысшего приопритета, и далее по нисходящей. Также важным моментом является возможность запустить nested runloop.
Что же прооисходит когда мы вызываем loadHTMLSTring: у UIWebView? Скорее всего делается performSelector c указанием не самого приоритетного RunLoopMode. В принципе, это правильно, так как загрузка контента в UIWebView задача нелегкая и требующая времени, если делать это мере сроллирования таблицы — может заметно протормаживать самое сроллирование. Но если контент достаточно легкий (простейший html в две строки, допустим)- загрузка пройдет достаточно быстро.
Для того чтобы выполнить эту загрузку не дожидаясь пока будут отработаны все touch cсобытия после вызова loadHTMLString нужно запустить nested run loop c меньшим приоритетом RunLoopMode — NSDefaultRunLoopMode.
CFRunLoopRunInMode((CFStringRef)NSDefaultRunLoopMode, 1NO);

Важно чтобы после того как WebView все таки загрузилась остановить этот RunLoop потому что иначе UI просто зависнет.
CFRunLoopRef runLoop = [[NSRunLoop currentRunLoop] getCFRunLoop];
CFRunLoopStop(runLoop);

UISynchedWebView


Давайте вынесем все что связано с запуском nested runloop и его остановкой в сам WebView. Сделаем subclass UIWebView, назове его UISynchedWebView и переопределим у него loadHTMLString так чтобы он после вызова базовой реализации запускал nested runloop. Возникает вопрос когда же его остановить? Остановить его нужно сразу после того как контент загрузился. Чтобы это определить нам нужно сделать наш новый WebView своим же делегатом, но так чтобы сохранить прозрачность для внешнего пользовательского кода. Для этого заведем переменную-член класса для внешнего делегата, переопределим его сеттеры и геттеры, и после вызова кода нашего делегата будем вызывать соответствующие методы внешнего делегата. Примерно так:

@interface UISynchedWebView : UIWebView <UIWebViewDelegate>
{
            id anotherDelegate;
}
@end


-(void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
    [self performSelector:@selector(stopRunLoop) withObject:nil afterDelay:.01];
 
    if([anotherDelegate respondsToSelector:@selector(webView:didFailLoadWithError:)])
        [anotherDelegate webView:webView didFailLoadWithError:error];
}
 
-(BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    if([anotherDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)])
        return [anotherDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    return YES;
}
 
-(void) webViewDidFinishLoad:(UIWebView *)webView
{
    [self performSelector:@selector(stopRunLoop) withObject:nil afterDelay:.01];
    if([anotherDelegate respondsToSelector:@selector(webViewDidFinishLoad:)])
        [anotherDelegate webViewDidFinishLoad:webView];
}
 
-(void) stopRunLoop
{
    CFRunLoopRef runLoop = [[NSRunLoop currentRunLoop] getCFRunLoop];
    CFRunLoopStop(runLoop);
 
}
 
-(void) webViewDidStartLoad:(UIWebView *)webView
{
    if([anotherDelegate respondsToSelector:@selector(webViewDidStartLoad:)])
        [anotherDelegate webViewDidStartLoad:webView];
}


Warning


На последок хотелось бы отметить что этот способ позволяет получить красиво форматирвоанный текст, где надо выделенный жирным, или подсвеченный бекграундом — в ячейках таблицы без особых усилий и имеет iOS3.x совместимость. Однако как было сказано, загрузка такого контента блокирует UI и если контент достаточно тяжелый — блокировка станет заметна и это будет очень плохо. Не везде этот способ можно использовать, будьте осторожны!

P.S. Демопроект можно взять тут.
Теги:
Хабы:
Всего голосов 8: ↑7 и ↓1+6
Комментарии18

Публикации

Истории

Работа

iOS разработчик
27 вакансий
Swift разработчик
32 вакансии

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область