Приветствую, Хабр!
Все началось с поиска решения задачи отображения форматированного текста внутри UITableViewCell, причем не строго заданного формата (тогда можно было бы использовать набор UILabel c заданным font) а произвольного. Да так, чтобы форматирование можно было задать простейшими html тегами. Решить такую задачу можно по-разному:
Как читеталь мог догадаться из заголовка статьи, я остановился на последнем способе, однако те из вас кто пробовал этот сопособ знают об очень неприятном подводном камне. Как от него избавиться я расскажу под катом.
И так, в чем же чуть этой проблемы — 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.
Важно чтобы после того как WebView все таки загрузилась остановить этот RunLoop потому что иначе UI просто зависнет.
Давайте вынесем все что связано с запуском nested runloop и его остановкой в сам WebView. Сделаем subclass UIWebView, назове его UISynchedWebView и переопределим у него loadHTMLString так чтобы он после вызова базовой реализации запускал nested runloop. Возникает вопрос когда же его остановить? Остановить его нужно сразу после того как контент загрузился. Чтобы это определить нам нужно сделать наш новый WebView своим же делегатом, но так чтобы сохранить прозрачность для внешнего пользовательского кода. Для этого заведем переменную-член класса для внешнего делегата, переопределим его сеттеры и геттеры, и после вызова кода нашего делегата будем вызывать соответствующие методы внешнего делегата. Примерно так:
На последок хотелось бы отметить что этот способ позволяет получить красиво форматирвоанный текст, где надо выделенный жирным, или подсвеченный бекграундом — в ячейках таблицы без особых усилий и имеет iOS3.x совместимость. Однако как было сказано, загрузка такого контента блокирует UI и если контент достаточно тяжелый — блокировка станет заметна и это будет очень плохо. Не везде этот способ можно использовать, будьте осторожны!
P.S. Демопроект можно взять тут.
Все началось с поиска решения задачи отображения форматированного текста внутри 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, 1, NO);
Важно чтобы после того как 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. Демопроект можно взять тут.