0

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

我想实现拉刷新,但是如果我在加载数据之前放开拉,应用程序会一直崩溃。(如果我保持拉动几秒钟一切都很好 - 如果我立即松开它会崩溃。

代码:

-(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:]: index 4 beyond bounds for empty array”

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

4

2 回答 2

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,它可以更好地协调这些网络请求与您可能在其他地方同时进行的其他网络请求。

But the main observations here are (a) don't update your model until the refresh is done and you're ready to update the UI; and (b) make sure you don't initiate a new refresh while the prior one is in progress (or, if you really need it, move to an operation queue model where you make cancelable NSOperation subclasses, and then you could theoretically cancel the prior request, if any, prior to issuing another update request).


Completely unrelated to the question at hand, but in my first code snippet, you'll see that I moved the setting of the _imageForCalendarType out of this block (as you're always setting it to the same thing) and into viewDidLoad. I also eliminated this unnecessary line:

_imageForCalendarType = [[NSDictionary alloc]init];

You discard this instantiated dictionary for the dictionary literal in the next line, so the above line is not needed.

Frankly, you probably shouldn't even have a dictionary of UIImage objects anyway, but rather just a dictionary of image names, and have cellForRowAtIndexPath instantiate the UIImage there. It probably doesn't matter when you have only three images, but if you ever had more, the existing array of UIImage objects construct could be problematic in memory pressure situations. Yes, you could insert the appropriate didReceiveMemoryWarning handling, but it's far simpler to just never maintain a dictionary with UIImage objects in the first place.

于 2013-10-30T14:32:08.950 回答
0

Since you are using sortedStartDates to publish your table, please construct this object in dispatch_sync GCD block instead of dispatch_async GCD block.

于 2013-10-30T15:44:38.457 回答