拉动刷新崩溃的应用程序

时间:2013-10-30 13:40:30

标签: ios uitableview ios7 pull-to-refresh

我有一个应用,可以从3个共享的Google日历中获取事件,并将其显示在表格视图中。

我想实现pull to refresh,但是如果我在数据加载之前放弃了拉动,应用程序会继续崩溃。 (如果我按住拉动几秒钟,一切都很好 - 如果我立即放开它会崩溃。

代码:

-(void)viewDidLoad
{
    [super viewDidLoad];    
    UIRefreshControl *refresh = [[UIRefreshControl alloc] init];
    refresh.attributedTitle = [[NSAttributedString alloc] initWithString:@"Pull to Refresh"];
    [refresh addTarget:self action:@selector(getEvents) forControlEvents:UIControlEventValueChanged];
    self.refreshControl = refresh;
    startDates = [[NSMutableArray alloc] init];
    [self getEvents];
}

- (void)stopRefresh
{
    [self.refreshControl endRefreshing];
}

-(void)getEvents
{
    [startDates removeAllObjects];
    startDates = [NSMutableArray array];
    sectionEntries = [NSMutableArray array];
    entries = [NSMutableArray array];
    sortedStartDates = [[NSArray alloc]init];
    _imageForCalendarType = [[NSDictionary alloc]init];
    _imageForCalendarType = @{
                              @"The Irish House Music Calendar" : [UIImage imageNamed:@"music.png"]
                              ,   @"FixedEvents-Student Night"  : [UIImage imageNamed:@"student.png"]
                              ,   @"FixedEvents-Ladies Night"         : [UIImage imageNamed:@"cocktail.png"]
                              ,   @"AppTest"         : [UIImage imageNamed:@"football.png"]
                              };
    dispatch_async(kBgQueue, ^{

        NSData* data = [NSData dataWithContentsOfURL:sportsCalendarURL];
        [self performSelectorOnMainThread:@selector(fetchedData:) withObject:data waitUntilDone:YES];

        NSData* data2 = [NSData dataWithContentsOfURL:musicCalendarURL];
        [self performSelectorOnMainThread:@selector(fetchedData:) withObject:data2 waitUntilDone:YES];

        NSData* data3 = [NSData dataWithContentsOfURL:fixedCalendarURL];
        [self performSelectorOnMainThread:@selector(fetchedData:) withObject:data3 waitUntilDone:YES];

        // Reload table view - UI operation, so must be run on main thread
        dispatch_async(dispatch_get_main_queue(), ^{

            sortedStartDates = [startDates sortedArrayUsingSelector:@selector(compare:)];
            [self.tableView reloadData];
            [self performSelector:@selector(stopRefresh) withObject:nil afterDelay:2.5];
        });
    });


}

它在cellForRowAtIndexPath方法的这一行给出了一个SIGABRT错误:

 NSInteger index = [self getRow:sortedStartDates[indexPath.section]];  // get correct index for sectionEntries

错误: *由于未捕获的异常'NSRangeException'而终止应用程序,原因:'* - [__ NSArrayI objectAtIndex:]:索引4超出空数组的界限'

似乎错误是因为我的startDates NSMutableArray中没有数据,但如果我注释行[startDates removeAllObjects]我会得到冗余单元格。

2 个答案:

答案 0 :(得分:3)

至少,我建议检查以确保刷新尚未进行。您可能还想更改getEvents以将刷新控制作为参数,并相应地更新下拉(因此用户将知道刷新正在进行中):

- (void)viewDidLoad
{
    [super viewDidLoad];

    _imageForCalendarType = @{
                              @"The Irish House Music Calendar" : [UIImage imageNamed:@"music.png"]
                          ,   @"FixedEvents-Student Night"      : [UIImage imageNamed:@"student.png"]
                          ,   @"FixedEvents-Ladies Night"       : [UIImage imageNamed:@"cocktail.png"]
                          ,   @"AppTest"                        : [UIImage imageNamed:@"football.png"]
                          };

    UIRefreshControl *refresh = [[UIRefreshControl alloc] init];
    refresh.attributedTitle = [[NSAttributedString alloc] initWithString:@"Pull to Refresh"];
    [refresh addTarget:self action:@selector(getEvents:) forControlEvents:UIControlEventValueChanged];
    self.refreshControl = refresh;
    [self getEvents:refresh];
}

- (void)getEvents:(UIRefreshControl *)refresh
{
    static BOOL refreshInProgress = NO;

    if (!refreshInProgress)
    {
        refreshInProgress = YES;

        refresh.attributedTitle = [[NSAttributedString alloc] initWithString:@"Refreshing"]; // let the user know refresh is in progress

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            // get the data here

            dispatch_async(dispatch_get_main_queue(), ^{

                // when done, update the model and the UI here

                refresh.attributedTitle = [[NSAttributedString alloc] initWithString:@"Pull to Refresh"]; // reset the message
                [refresh endRefreshing];

                refreshInProgress = NO;
            });
        });
    }
}

但是,您应该非常小心地异步更新模型数据(因为您的主队列可能会在您的更新正在进行时尝试从模型中检索信息)。你真的应该推迟模型的更新,直到最后一次调度到主队列。但是不要在异步过程中更新模型,否则模型和UI可能会暂时以不一致的状态结束。

此外,作为一种改进,您可能希望同时检索这三个数据源,并且您可能会观察到可识别的性能改进。

- (void)getEvents:(UIRefreshControl *)refresh
{
    static BOOL refreshInProgress = NO;

    if (!refreshInProgress)
    {
        refreshInProgress = YES;

        refresh.attributedTitle = [[NSAttributedString alloc] initWithString:@"Refreshing"]; // let the user know refresh is in progress

        // get the data here

        __block NSData *data1 = nil;
        __block NSData *data2 = nil;
        __block NSData *data3 = nil;

        dispatch_queue_t queue = dispatch_queue_create([[[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:@".network"] UTF8String], DISPATCH_QUEUE_CONCURRENT);

        dispatch_async(queue, ^{
            data1 = [NSData dataWithContentsOfURL:sportsCalendarURL];
        });

        dispatch_async(queue, ^{
            data2 = [NSData dataWithContentsOfURL:musicCalendarURL];
        });

        dispatch_async(queue, ^{
            data3 = [NSData dataWithContentsOfURL:fixedCalendarURL];
        });

        // use dispatch barrier here, which will only fire when the previous three requests are done

        dispatch_barrier_async(queue, ^{

            // update the UI here

            dispatch_async(dispatch_get_main_queue(), ^{

                startDates     = [NSMutableArray array];
                sectionEntries = [NSMutableArray array];
                entries        = [NSMutableArray array];

                [self fetchedData:data1];
                [self fetchedData:data2];
                [self fetchedData:data3];

                refresh.attributedTitle = [[NSAttributedString alloc] initWithString:@"Pull to Refresh"]; // reset the message
                [refresh endRefreshing];

                sortedStartDates = [startDates sortedArrayUsingSelector:@selector(compare:)];
                [self.tableView reloadData];

                refreshInProgress = NO;
            });
        });
    }
}

如果您只有三个数据源,则可能会使用GCD并发队列,但如果您可能有更多数据源,则可能需要使用可以限制并发请求数的操作队列。此外,您可以考虑使用AFNetworking,这可以更好地协调这些网络请求与您可能同时在其他地方进行的其他网络请求。

但这里的主要观察结果是(a)在刷新完成并且您已准备好更新UI​​之前不要更新您的模型; (b)确保在前一个刷新过程中不启动新刷新(或者,如果你真的需要它,移动到你可以生成可取消NSOperation子类的操作队列模型,然后你理论上可以在发出另一个更新请求之前取消先前的请求(如果有的话)。


完全与手头的问题无关,但在我的第一个代码段中,您会看到我将_imageForCalendarType的设置移出此块(因为您始终将其设置为相同的内容)并进入viewDidLoad。我也消除了这条不必要的路线:

_imageForCalendarType = [[NSDictionary alloc]init];

您丢弃下一行中字典文字的实例化字典,因此不需要上面的行。

坦率地说,你可能甚至不应该有一个UIImage对象的字典,而只是一个图像名称字典,并cellForRowAtIndexPath在那里实例化UIImage。当你只有三个图像时可能没关系,但是如果你有更多的图像,现有的UIImage对象构造数组在内存压力情况下可能会出现问题。是的,您可以插入适当的didReceiveMemoryWarning处理,但是首先从不维护带有UIImage个对象的字典要简单得多。

答案 1 :(得分:0)

由于您使用sortedStartDates发布表,请在dispatch_sync GCD块中构造此对象,而不是dispatch_async GCD块。