Как эффективно обновить UITableView с помощью анимации?

мое приложение iPad имеет UITableView, заполненный из канала. Как и большинство читателей RSS, он отображает список ссылок с блога в обратном хронологическом порядке, с их названиями и резюме каждый пост. Лента часто обновляется и довольно большая, около 500 сообщений. Я использую libxml2 push parsing для эффективной загрузки и анализа фида в подклассе NSOperation, построения объектов ввода и обновления базы данных по мере продвижения. Но тогда мне нужно обновить UITableView с изменениями.

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

Я мог бы просто позвонить reloadData когда все сообщения были проанализированы, но это не очень удобно: нет анимации, чтобы указать, что что-то изменилось, просто вспышка и новые данные есть. Я бы предпочел, чтобы он анимировался, чтобы показать, что новые сообщения добавляются, а старые удаляются. Существующие должности, которые не удаляются из ленты, должны быть сдвинуты вниз по таблице новыми должностями, появляющимися вверху.

Я знаю, что это возможно. Byline, чтобы дать один пример, делает красивую работу. Каждый пост добавляется или удаляется из UITableView по одному за раз без пробелов, показывающих фон таблицы. Все, не делая UI ни в малейшей степени невосприимчивым. Как это делается??

моя последняя попытка обновить таблицу только после того, как все сообщения были проанализированы (парсер довольно быстрый, поэтому это не большая задержка). Затем он загружает существующие сообщения в NSDictionary, сопоставляя их идентификаторы своим индексам в массив, используемый в качестве источника данных таблицы. Затем он перебирает каждый объект в недавно проанализированном массиве сообщений, добавляя NSIndexPath для каждого в массивы, которые позже передаются в -insertRowsAtIndexPaths:withRowAnimation:, -deleteRowsAtIndexPaths:withRowAnimation: и -reloadRowsAtIndexPaths:withRowAnimation: при необходимости вставлять, удалять, перемещать или обновлять ячейки. Для 500 сообщений это занимает около 4 секунд для обновления, при этом пользовательский интерфейс полностью не отвечает. Это время используется почти исключительно для анимированных обновлений UITableView; итерация по двум массивам сообщений занимает очень мало время.

затем я изменил его так, чтобы они были обновлены без анимации, и у меня есть отдельные массивы для вставки/удаления/перезагрузки с анимацией только для позиций строк, соответствующих текущим видимым строкам. Это лучше, но пробелы появляются по мере удаления сообщений и добавления новых.

извините, это так многословно, но вот результат:

как я могу обновить UITableView, с новыми ячейками нажата, другие оттолкнулись, а третьи переехали из одной позиции в другую, С до 500 ячеек в UITableView (6-8 видны одновременно), и каждая анимация происходит последовательно, все время, пока пользовательский интерфейс остается полностью отзывчивым?

2 ответов


этот вопрос на самом деле имеет три ответа. То есть, есть три части этого вопроса:

  1. как сохранять отзывчивость UI
  2. как сохранить обновление быстро
  3. как сделать анимацию обновления таблицы гладкой

UI отзывчивость

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

это делается благодаря примеру кода, отправленного мне Byline автор Milo Bird, который я затем интегрировал в Дейв Дрибин ' s DDInvocationGrabber. Этот интерфейс упрощает очередь метода, вызываемого на следующей доступной итерации цикла основного события:

[[(id)delegate queueOnMainThread]
    parserParsedEntries:parsedEntries
               inPortal:parsedPortal];

мне очень нравится, как легко это использовать этот метод. Парсер теперь использует его для вызова всех методов делегата, большинство из которых обновить UI. Я выпустил этот код на GitHub.

производительность

Что касается производительности, я изначально обновлял одну строку UITableView за раз. Это было эффективно, но несколько неэффективно. Я вернулся и изучил XMLPerformance пример, где я заметил, что парсер ждал, пока он не собрал 10 элементов перед отправкой в основной поток для обновления таблицы. Это было ключом к поддержанию производительности без блокировки пользовательского интерфейса путем обновления всех 500 строк сразу. Я играл с обновлением 1, 10 и всех 500 строк за один вызов, и обновление 10, казалось, предлагало лучший компромисс между производительностью и блокировкой пользовательского интерфейса. пять, наверное, тоже неплохо.

анимация

и, наконец, есть анимация. Смотреть "освоение табличных представлений" WWDC 2010 сессия, я понял, что мое использование deleteRowsAtIndexPaths:withRowAnimation: и updateRowsAtIndexPaths:withRowAnimation: методы были неправильными. Я следил за тем, где что-то должно быть добавлено и удалено в таблице, и соответствующим образом корректировал индексы, но оказалось, что это не обязательно. Внутри блока обновления таблицы нужно только ссылаться на индекс строки из до обновление, независимо от того, сколько может быть вставлено или удалено, чтобы изменить свою позицию. Блок обновления, по-видимому, делает всю эту бухгалтерию для вы. (Перейдите к отметке 8: 45 в видео для ключевого примера).

таким образом, метод делегата, который обновляет таблицу для количества записей, переданных ему синтаксическим анализатором (в настоящее время 10-at-a-time), теперь явно отслеживает позиции строк, которые будут обновлены или удалены из до блок обновления, например:

NSMutableDictionary *oldIndexFor = [NSMutableDictionary dictionaryWithCapacity:posts.count];
int i = 0;
for (PostModel *e in  posts) {
    [oldIndexFor setObject:[NSNumber numberWithInt:i++] forKey:e.ident];
}

NSMutableArray *insertPaths = [NSMutableArray array];
NSMutableArray *deletePaths = [NSMutableArray array];
NSMutableArray *reloadPaths = [NSMutableArray array];
BOOL modified = NO;

for (PostModel *entry in entries) {
    NSNumber *num = [oldIndexFor objectForKey:entry.ident];
    NSIndexPath *path = [NSIndexPath indexPathForRow:currentPostIndex inSection:0];
    if (num == nil) {
        modified = YES;
        [insertPaths addObject:path];
        [posts insertObject:entry atIndex:currentPostIndex];
    } else {
        // Find its current position in the array.
        NSUInteger foundAt = [posts indexOfObject:entry];
        if (foundAt == currentPostIndex) {
            // Reload it if it has changed.
            if (entry.savedState != PostModelSavedStateUnmodified) {
                modified = YES;
                [posts replaceObjectAtIndex:foundAt withObject:entry];
                [reloadPaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
            }
        } else {
            // Move it.
            modified = YES;
            [posts removeObjectAtIndex:foundAt];
            [posts insertObject:entry atIndex:currentPostIndex];
            [insertPaths addObject:path];
            [deletePaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
        }
    }
    currentPostIndex++;
}
if (modified) {
    [tableView beginUpdates];
    [tableView insertRowsAtIndexPaths:insertPaths withRowAnimation:UITableViewRowAnimationTop];
    [tableView deleteRowsAtIndexPaths:deletePaths withRowAnimation:UITableViewRowAnimationBottom];
    [tableView reloadRowsAtIndexPaths:reloadPaths withRowAnimation:UITableViewRowAnimationFade];
    [tableView endUpdates];
}

комментарии приветствуются. Вполне возможно, что есть более эффективные способы сделать это (использование -[NSArray indexOfObject:] особенно подозрительны для меня), и что я, возможно, пропустил какую-то другую тонкость.

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


ты пробовал [tableView beginUpdates]; и [tableView endUpdate];?