多个NSUrlRequest获取Web服务的不同页面以在TableView中加载更多数据

时间:2013-04-22 09:29:44

标签: ios objective-c nsurlrequest nsmutableurlrequest

我有一个简单的iPhone应用程序,它从rss feed解析数据(标题,图像等)并显示在tableview中。

viewDidLoad有一个初始计数器值,可以通过调用fetchEntriesNew方法到达feed的第一页并加载到tableview中:

- (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];
}

当用户到达tableview的底部时,我使用以下内容到达Feed的下一页并加载到首先加载的第一页的底部:

- (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];
    }
}

UPDATE2:以下是一个更容易理解的错误描述,我无法解决:例如,rss Feed的每个页面中有10个条目。应用程序启动,标题和其他标签立即加载,图像开始懒洋洋地加载,最后完成。到现在为止还挺好。用户滚动到达底部,到达底部将使用滚动委托方法,计数器从1增加到2,告诉fetchEntriesNew方法到达rss feed的第二页。程序将开始加载之前获取的前10个底部的下10个条目。这可以继续,程序将在每次用户滚动并到达底部时再获取10个条目,并且新行将被放置在先前获取的那些下面。到目前为止一切都很好。

现在让我们说当前第3页的用户已经完全加载了图像。由于第3页被完全加载,这意味着目前在tableview中有30个条目。用户现在滚动到底部,计数器增加,并且tableview开始填充前30个条目底部的rss feed的第4页的新行。标题快速填充,从而构建行,当图像被下载(尚未完全下载)时,用户再次快速移动到底部,而不是在第4页底部加载第5页,它将破坏第4页目前正在下载并开始再次加载第4个。

它应该做的是,当用户到达桌面视图的底部时,它应该保留下一页的标题等,无论前一页的图像是否处于下载过程中。

我的项目中的下载和持久数据没有问题,所有数据都在应用程序运行之间保持不变。

有人可以帮我指出正确的方向。提前谢谢。

更新3:根据@ Sergio的回答,这就是我的所作所为:

1)添加了对archiveRootObject的另一个调用[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];在[channelCopy addItemsFromChannel:obj];

之后

此时,它不会一次又一次地破坏和重新加载同一批次,正是我想要的。但是,如果我多次滚动到达下一页而没有完全加载上一页的图像,则它不会保留图像。

2)我不知道如何在答案中解释如何使用Bool。这就是我做的:添加@property Bool myBool;在TheFeedStore中,合成它并在新添加的archiveRootObject:channelCopy之后将其设置为NO,并在fetchEntries方法的最开始在ListViewController中将其设置为YES。它不起作用。

3)我也意识到我处理整个问题的方式是性能不好。虽然我不知道如何使用缓存外的图像并将其作为缓存处理。您是否建议为图像使用单独的归档文件?

非常感谢所有为解决我的问题做出贡献的人。

3 个答案:

答案 0 :(得分:3)

如果您考虑this older question of yoursthe solution I proposed,则可以理解您的问题。

具体来说,关键位与您持久保存信息(RSS信息+图像)的方式有关,即通过将整个channel存档到磁盘上的文件:

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

现在,如果你看一下fetchEntriesNew:,你在那里做的第一件事是摧毁你当前的频道。如果在频道被保存到磁盘之前发生这种情况,则会进入一种无限循环。

据我所知,您目前正在图片下载结束时保留您的频道(根据我的原始建议)。

您应该做的是在读取Feed之后和开始下载图像之前保留频道(当然,在图像下载结束时也应该保留它)。

所以,如果你从旧的要点中获取这个片段:

[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);

只要您没有足够快地滚动以便在读取Feed(没有图像)之前销毁通道,这将使工作正常工作。要解决此问题,您应该在致电TheFeedStore时为YES课程添加一个bool,并在执行新添加的fetchWebService后立即重置。

这将解决您的问题。

我还要说,从设计/架构的角度来看,管理持久性的方式存在很大问题。实际上,您在磁盘上有一个文件,您使用archiveRootObject:channelCopy原子编写。这种架构本质上是有风险的&#34;从多线程的角度来看,你还应该设法避免对共享存储的并发访问没有破坏性影响(例如:你将你的频道存档到第4页的磁盘,与第1页的图像同时存档已完全下载,因此您尝试将它们保存到同一文件中。)

图像处理的另一种方法是将图像存储在存档文件之外,并将它们视为一种缓存。这样可以解决并发问题,并且还可以消除每个页面两次存档通道所带来的性能损失(首次读取数据时以及后来图像进入时)。

希望这有帮助。

更新:

  

此时,它不会一次又一次地破坏和重新加载同一批次,正是我想要的。但是,如果我多次滚动到达下一页而没有完全加载上一页的图像,它就不会保留图像。

这正是我的意思,说您的架构(共享存档/并发访问)可能会导致问题。

您有几种选择:使用Core Data / sqlite;或者,更轻松地将每个图像存储在自己的文件中。在后一种情况下,您可以执行以下操作:

  1. 检索时,为每个图像分配一个文件名(这可以是提要条目的ID或序号或其他)并将图像数据存储在那里;

  2. 在档案中存储图像的URL和应存储的文件名;

  3. 当您需要访问图片时,您无法直接从存档字典中获取图片;相反,你从它获得文件名然后从磁盘读取文件(如果可用);

  4. 此更改不会影响您当前的rss /图像检索实现,但只会影响您保存图像的方式,并在需要时访问它们(我的意思是,这似乎很容易改变)。

  5.   

    2)我不知道如何在答案中解释如何使用Bool。

    1. 向TheFeedStore添加archiveRootObject bool;

    2. 在您isDownloading方法中将其设置为YES,就在fetchWebService:之前;

    3. 在第一次归档Feed后,在完成后将其设置为[connection start],然后传递给连接对象(再次在NO中)(这是你已经在做的); < / p>

    4. 在您的fetchWebService:中,一开始就执行:

      scrollViewDidEndDecelerating:

      以便在刷新过程中不刷新rss提要。

    5. 如果有帮助,请告诉我。

      新更新:

      让我简要介绍一下如何处理将图像存储在文件中。

      在您的RSSItem类中,定义:

          if ([TheFeedStore sharedStore].isDownloading)
              return;
      

      @property (nonatomic, readonly) UIImage *thumbnail; @property (nonatomic, strong) NSString *thumbFile; 是托管图像的本地文件的路径。获得图片网址(thumbFile)后,您可以获取,例如和MD5哈希值,并将其用作本地图片文件名:

      getFirstImageUrl

      (MD5String是您可以谷歌搜索的类别)。

      然后,在NSString* imageURLString = [self getFirstImageUrl:someString]; .... self.thumbFile = [imageURLString MD5String]; 中,您将在本地存储图像文件:

      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; } 以使其正常工作。

      如你所见,这种方法非常简单&#34;实施。这不是一个优化的解决方案,只是让您的应用程序使用其当前架构的快速方法。

      为了完整性,thumbFile类别:

      MD5String

答案 1 :(得分:2)

您实际上要做的是在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];

答案 2 :(得分:1)

简单的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