Подтвердить что ты не робот

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

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

Это сделано благодаря примеру кода, присланного мне автором Byline Мило Бердом, который я затем интегрировал в Dave Dribin DDInvocationGrabber. Этот интерфейс позволяет очень просто поставить в очередь метод, который будет вызван на следующей доступной итерации цикла основного события:

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

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

Спектакль

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

Анимация

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

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

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:] особенно подозрительно для меня), и что я, возможно, пропустил некоторые другие тонкости.

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

Ответ 2

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