Несколько NSUrlRequests для получения разных страниц веб-службы для загрузки дополнительных данных в TableView

У меня есть простое приложение для iPhone, которое анализирует данные (заголовки, изображения и т. д.) из RSS-канала и показывает их в виде таблицы.

ViewDidLoad имеет начальное значение счетчика для достижения первой страницы канала и загрузки в табличном представлении путем вызова метода fetchEntriesNew:

- (void)viewDidLoad
{
    [super viewDidLoad];        
    counter = 1;    
    [self fetchEntriesNew:counter];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(dataSaved:)
                                              name:@"DataSaved" object:nil];
}


- (void) fetchEntriesNew:(NSInteger )pageNumber
{    
    channel = [[TheFeedStore sharedStore] fetchWebService:pageNumber withCompletion:^(RSSChannel *obj, NSError *err){

            if (!err) {
                int currentItemCount = [[channel items] count];
                channel = obj;
                int newItemCount = [[channel items] count];
                NSLog(@"Total Number Of Entries Are: %d", newItemCount);
                counter = (newItemCount / 10) + 1;
                NSLog(@"New Counter Should Be %d", counter);


                int itemDelta = newItemCount - currentItemCount;
                if (itemDelta > 0) {

                    NSMutableArray *rows = [NSMutableArray array];

                    for (int i = 0; i < itemDelta; i++) {
                        NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
                        [rows addObject:ip];
                    }
                    [[self tableView] insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
                    [aiView stopAnimating];

                }
            }        
    }];
    [[self tableView] reloadData];
}

Когда пользователь достигает нижней части табличного представления, я использую следующее, чтобы перейти к следующей странице канала и загрузить ее в нижней части первой страницы, которая была загружена первой:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
    if (endScrolling >= scrollView.contentSize.height)
    {
        NSLog(@"Scroll End Called");
        NSLog(@"New Counter NOW is %d", counter);
        [self fetchEntriesNew:counter];
    }
}

ОБНОВЛЕНИЕ 2: Вот более простое для понимания описание того, что не так, что я не могу решить: например, на каждой странице RSS-канала есть 10 записей. Приложение запускается, заголовки и другие метки загружаются немедленно, а изображения начинают загружаться лениво и, наконец, завершаются. Все идет нормально. Пользователь прокручивает, чтобы достичь нижней части, при достижении нижней части будет использоваться метод делегата прокрутки, а счетчик увеличивается с 1 до 2, сообщая методу fetchEntriesNew о достижении второй страницы RSS-канала. Программа начнет загрузку следующих 10 записей в нижней части первых 10 ранее выбранных. Это может продолжаться, и программа будет получать еще 10 записей каждый раз, когда пользователь прокручивает и достигает нижней части, а новые строки будут размещаться ниже ранее выбранных. Все идет нормально.

Теперь предположим, что пользователь в настоящее время находится на странице 3, которая полностью загружена изображениями. Поскольку страница 3 загружена полностью, это означает, что в настоящее время в таблице 30 записей. Теперь пользователь прокручивается вниз, счетчик увеличивается, и табличное представление начинает заполнять новые строки со страницы 4 rss-канала в нижней части первых 30 записей. Заголовки быстро заполняются, таким образом строятся строки, и пока изображения загружаются (еще не загружены полностью), пользователь снова быстро перемещается вниз, вместо загрузки 5-й страницы внизу 4-й, он уничтожит 4-е. который в настоящее время находится в процессе загрузки и снова начинает загружать 4-й.

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

Нет проблем с загрузкой и сохранением данных в моем проекте, и все данные сохраняются между запусками приложения.

Может кто-нибудь помочь указать мне в правильном направлении. Заранее спасибо.

ОБНОВЛЕНИЕ 3: Основываясь на ответе @Sergio, я сделал следующее:

1) Добавлен еще один вызов archiveRootObject [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath]; после [channelCopy addItemsFromChannel:obj];

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

2) Я не уверен, как использовать Bool, как он объяснил в ответе. Вот что я сделал: добавил @property Bool myBool; в TheFeedStore синтезировал его и установил для него значение NO после только что добавленного archiveRootObject:channelCopy и установил для него значение YES в ListViewController в самом начале метода fetchEntries. Это не сработало.

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

Большое спасибо всем людям, которые внесли свой вклад в решение моей проблемы.


person Jessica    schedule 22.04.2013    source источник
comment
Я запутался. Почему вызов reloadData не находится ВНУТРИ обратного вызова? Кроме того, вам это действительно нужно в первую очередь? Новые строки все равно получат вызов cellForRowAtIndexPath...   -  person samson    schedule 25.04.2013
comment
Это странно. Может быть, потому что вы вставляете строки в начало таблицы, а не в конец?   -  person samson    schedule 25.04.2013
comment
превосходно. Я полагаю, что это не поможет вам решить вашу проблему, хотя ... тем не менее, почему вы вставляете в начало таблицы? Вы можете поместить оператор журнала в cellForRowAtIndexPath и посмотреть, какие строки перезагружаются?   -  person samson    schedule 25.04.2013
comment
ну, кажется, вы вставляете строки, начиная с индекса 0, который будет первой строкой таблицы... в любом случае, это может быть несущественно.   -  person samson    schedule 25.04.2013


Ответы (3)


Вашу проблему можно понять, если вы рассмотрите этот ваш старый вопрос и решение, которое я предложил.

В частности, критический бит связан с тем, как вы сохраняете информацию (информация RSS + изображения), то есть через архивирование всего вашего channel в файл на диске:

        [channelCopy addItemsFromChannel:obj];
        [NSKeyedArchiver archiveRootObject:channelCopy toFile:pathOfCache];

Теперь, если вы посмотрите на fetchEntriesNew:, первое, что вы там сделаете, это уничтожите свой текущий канал. Если это происходит до того, как канал был сохранен на диск, вы входите в своего рода бесконечный цикл.

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

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

Итак, если вы возьмете этот фрагмент из моего старого содержания:

[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {

    if (!err) {

        [channelCopy addItemsFromChannel:obj];

        // ADDED
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
            [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
        });
    }
    block(channelCopy, err);

что вам нужно сделать, это добавить еще один вызов archiveRootObject:

[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {

    if (!err) {

        [channelCopy addItemsFromChannel:obj];
        [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];

        // ADDED
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
            [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
        });
    }
    block(channelCopy, err);

Это заставит все работать, пока вы не прокручиваете достаточно быстро, чтобы канал был уничтожен до того, как лента (без изображений) когда-либо будет прочитана. Чтобы исправить это, вы должны добавить bool к вашему классу TheFeedStore, который вы устанавливаете в YES при вызове fetchWebService и сбрасываете сразу после выполнения только что добавленного archiveRootObject:channelCopy.

Это решит ваши проблемы.

Позвольте мне также сказать, что с точки зрения дизайна/архитектуры у вас есть большая проблема с тем, как вы управляете сохраняемостью. Действительно, у вас есть единственный файл на диске, который вы записываете атомарно, используя archiveRootObject. Эта архитектура по своей сути является «рискованной» с точки зрения многопоточности, и вам также следует разработать способ, чтобы одновременный доступ к общему хранилищу не имел разрушительных последствий (например, вы архивируете свой канал на диск для страницы 4 одновременно). время, когда изображения для страницы 1 были полностью загружены, поэтому вы пытаетесь сохранить их в том же файле).

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

Надеюсь это поможет.

ОБНОВИТЬ:

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

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

У вас есть несколько вариантов: используйте Core Data/sqlite; или, что проще, хранить каждое изображение в отдельном файле. В последнем случае вы можете сделать следующее:

  1. при извлечении назначьте каждому изображению имя файла (это может быть идентификатор записи канала, порядковый номер или что-то еще) и сохраните данные изображения там;

  2. хранить в архиве как URL изображения, так и имя файла, в котором оно должно храниться;

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

  4. это изменение не повлияет на вашу текущую реализацию поиска rss/image, а только на то, как вы сохраняете изображения и получаете к ним доступ при необходимости (я имею в виду, что это кажется довольно простым изменением).

2) Я не уверен, как использовать Bool, как он объяснил в ответе.

  1. добавить bool isDownloading в TheFeedStore;

  2. установите его на YES в методе fetchWebService: непосредственно перед выполнением [connection start];

  3. установите его на NO в блоке завершения, который вы передаете объекту соединения (снова в fetchWebService:) сразу после архивации фида в первый раз (это вы уже делаете);

  4. в вашем scrollViewDidEndDecelerating: в самом начале сделайте:

        if ([TheFeedStore sharedStore].isDownloading)
            return;
    

    чтобы вы не обновляли rss-канал, пока идет обновление.

Позвольте мне знать, если это помогает.

НОВОЕ ОБНОВЛЕНИЕ:

Позвольте мне набросать, как вы можете хранить изображения в файлах.

В вашем классе RSSItem определите:

@property (nonatomic, readonly) UIImage *thumbnail;
@property (nonatomic, strong) NSString *thumbFile;

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

NSString* imageURLString = [self getFirstImageUrl:someString];
....
self.thumbFile = [imageURLString MD5String];

(MD5String — это категория, которую вы можете найти в Google).

Затем в downloadThumbnails вы должны сохранить файл изображения локально:

    NSMutableData *tempData = [NSData dataWithContentsOfURL:finalUrl];
    [tempData writeToFile:[self cachedFileURLFromFileName:self.thumbFile] atomically:YES];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"DataSaved" object:nil];

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

- (UIImage *)thumbnail
{
    NSData* d = [NSData dataWithContentsOfURL:[self cachedFileURLFromFileName:self.thumbFile]];
    return [[UIImage alloc] initWithData:d];
}

в этом фрагменте cachedFileURLFromFileName: определяется как:

- (NSURL*)cachedFileURLFromFileName:(NSString*)filename {

NSFileManager *fileManager = [[NSFileManager alloc] init];
NSArray *fileArray = [fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask];

NSURL* cacheURL = (NSURL*)[fileArray lastObject];
if(cacheURL)
{
    return [cacheURL URLByAppendingPathComponent:filename];
}
return nil;
}

Конечно, thumbFile должен быть сохранен, чтобы это работало.

Как видите, этот подход довольно «легко реализовать». Это не оптимизированное решение, а просто быстрый способ заставить ваше приложение работать с его текущей архитектурой.

Для полноты, категория MD5String:

@interface NSString (MD5)

- (NSString *)MD5String;

@end

@implementation NSString (MD5)

- (NSString *)MD5String {
const char *cstr = [self UTF8String];
unsigned char result[16];
CC_MD5(cstr, strlen(cstr), result);

return [NSString stringWithFormat:
        @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
        result[0], result[1], result[2], result[3],
        result[4], result[5], result[6], result[7],
        result[8], result[9], result[10], result[11],
        result[12], result[13], result[14], result[15]
        ];  
}

@end
person sergio    schedule 24.04.2013
comment
bool не имеет ничего общего с изображениями. это только предотвращает многократную загрузку страницы канала (представьте себе медленное соединение и быструю прокрутку вниз). для изображений у вас есть только основные данные или файловый кеш... - person sergio; 26.04.2013
comment
Использование Core Data потребует полной перестройки вашего приложения. Использование Core Data только для файлов изображений не имеет реального смысла и реальных преимуществ. Взгляните на мою схематичную реализацию другого подхода, который я предложил. - person sergio; 26.04.2013
comment
Большое спасибо за помощь. в методе downloadThumbnails я использовал writeToUrl вместо writeToFile, в противном случае отображается предупреждение. Теперь никаких предупреждений, но программа вылетает из-за этого -[NSURL URLByAppendingPathComponent:]: компонент, компоненты или pathExtension не могут быть нулевыми.' - person Jessica; 27.04.2013
comment
проверьте значение self.thumbFile. правильно ли вы инициализируете его перед вызовом метода, который дает сбой? Я предложил только один возможный способ определить self.thumbFile с помощью хэша MD5, но если вы обнаружите, что это сложно, вы можете просто использовать счетчик последовательности (т. Е. Вы называете свой файл 1, 2, 3, 4 и т. д.) - person sergio; 27.04.2013

Что вы на самом деле пытаетесь сделать, так это реализовать пейджинг в UITableView

Теперь это очень просто, и лучше всего реализовать пейджинг в методе UITableView делегата cellForRowAtIndexPath вместо того, чтобы делать это в методе UIScrollView scrollViewDidEndDecelerating делегата.

Вот моя реализация пейджинга, и я считаю, что она должна отлично работать и для вас:

Прежде всего, у меня есть константы реализации, связанные с пейджингом:

//paging step size (how many items we get each time)
#define kPageStep 30
//auto paging offset (this means when we reach offset autopaging kicks in, i.e. 10 items before the end of list)
#define kPageBegin 10

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

Вот как я делаю пейджинг:

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSUInteger row = [indexPath row];
    int section = indexPath.section-1;

    while (section>=0) {
        row+= [self.tableView numberOfRowsInSection:section];
        section--;
    }

    if (row+kPageBegin>=currentItems && !isLoadingNewItems && currentItems+1<maxItems) {
        //begin request
        [self LoadMoreItems];
    }
......
}
  • currentItems — это целое число, содержащее количество текущих элементов источника данных tableView.

  • isLoadingNewItems — это логическое значение, которое отмечает, извлекаются ли элементы в данный момент, поэтому мы не создаем экземпляр другого запроса, пока загружаем следующий пакет с сервера.

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

Вы можете опустить проверку maxItems, если не хотите иметь ограничение.

и в моем коде загрузки подкачки я устанавливаю флаг isLoadingNewItems в значение true и возвращаю его в значение false после получения данных с сервера.

Итак, в вашей ситуации это будет выглядеть так:

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSUInteger row = [indexPath row];
    int section = indexPath.section-1;

    while (section>=0) {
        row+= [self.tableView numberOfRowsInSection:section];
        section--;
    }

    if (row+kPageBegin>=counter && !isDowloading) {
        //begin request
        isDowloading = YES;
        [self fetchEntriesNew:counter];
    }
......
}

Также нет необходимости перезагружать всю таблицу после добавления новых строк. Просто используйте это:

for (int i = 0; i < itemDelta; i++) {
    NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
    [rows addObject:ip];
}

[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
person Lefteris    schedule 24.04.2013
comment
ах. я вижу, что ваше решение более сложное, как Instagram, который начинает загружать следующую партию, когда пользователь проходит половину первой партии. Но моя проблема в другом, проверьте ответ Серджио, который я сейчас тестирую. - person Jessica; 25.04.2013

Простого BOOL достаточно, чтобы избежать повторяющихся вызовов:

BOOL isDowloading;

Когда загрузка будет завершена, установите для него значение NO. Когда он входит сюда:

 if (endScrolling >= scrollView.contentSize.height)
    {
        NSLog(@"Scroll End Called");
        NSLog(@"New Counter NOW is %d", counter);
        [self fetchEntriesNew:counter];
    }

поставить его на YES. Также не забудьте установить для него значение NO, когда запросы не выполняются.

Редактировать 1:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
    if (endScrolling >= scrollView.contentSize.height)
    {
        if(!isDowloading)
        {
           isDownloading = YES;
           NSLog(@"Scroll End Called");
           NSLog(@"New Counter NOW is %d", counter);
           [self fetchEntriesNew:counter];
        }
    }
}

И когда вы закончите выборку, просто снова установите NO.

person Rui Peres    schedule 22.04.2013
comment
Нет, не будет. Если вы установите, например, NO в viewDidLoad, и NO, когда вы действительно закончите загрузку (или не удалось), это сработает) - person Rui Peres; 22.04.2013
comment
Я сделал именно то, что вы сказали, но это не сработало. Теперь я начинаю полагать, что проблемы кроются где-то еще, поскольку код подключения отличается, а изображения обрабатываются в отдельном GCD независимо от методов подключения. Ваше решение сработало бы нормально, если бы изображения не использовались. - person Jessica; 22.04.2013
comment
Где вы поставили isDownloading = NO кроме viewDidLoad? Можете ли вы показать мне суть с ним? - person Rui Peres; 22.04.2013
comment
проверьте наличие комментариев /////Попробовал здесь в gist.github.com/jessicamoore112/5433625 - person Jessica; 22.04.2013