Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does UITableView lose track of section header when scrolling and updating at the same time?

I am running into a bug where a table view may lose track of a section header, creating a new section header in its place. (See the following image; the right screen shot shows the lost header.)

enter image description here

Although I do not know the exact cause, this seems to happen when I programmatically scroll the table view and update it at the same time by calling "beginUpdates" and "endUpdates." Can anyone explain why the table view loses track of the section header? When exactly does this happen?

Below, I have provided code for a view controller that demonstrates this problem. The code displays a table view whose data model is an array of arrays of strings. When you press the add button, a new row is added to the table and is scrolled into view. After a brief moment of being added, the new row is made taller than the other cells to show that it is the newly added cell. To reproduce the bug, press the "Add" button several times and then scroll to the top. (I tested this in the 4-inch 64-bit iPhone simulator.)

The code that may be causing the bug is in the "pressedAddButton:" method because it is there that I scroll and update the table view at the same time. (EDIT: Much of the other code is just boiler plate to set up the data model and to provide the data source and delegate methods for the table view.)

Note: To get the code to run, put the view controller inside a UINavigationController and display the navigation controller:

ABCTestViewController * testViewController = [[ABCTestViewController alloc] initWithNibName:nil bundle:nil];
UINavigationController * navigationController = [[UINavigationController alloc] initWithRootViewController:testViewController];
// Now show the navigation controller.

In ABCTestViewController.h:

#import <UIKit/UIKit.h>

@interface ABCTestViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>

@property (weak, nonatomic) UITableView * tableView;

// The data model is an array of arrays of strings.
@property (strong, nonatomic) NSMutableArray * dataModel;

// The special row will be shown as being taller than the other rows.
@property (strong, nonatomic) NSIndexPath * indexPathOfSpecialRow;

@end

In ABCTestViewController.m:

#import "ABCTestViewController.h"

#import <QuartzCore/QuartzCore.h>

@interface ABCTestViewController ()

@end

@implementation ABCTestViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        [self initializeNavigationItem];
        [self initializeDataModel];
    }
    return self;
}

- (void)initializeNavigationItem
{
    self.title = @"Test";

    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Add" style:UIBarButtonItemStylePlain target:self action:@selector(pressedAddButton:)];
}

- (void)initializeDataModel
{
    self->_dataModel = [[NSMutableArray alloc] init];

    // Add strings to section 0.
    NSMutableArray * section0 = [[self class] stringsForSection:0 numberOfStrings:2];
    [self.dataModel addObject:section0];

    // Add strings to section 1.
    NSMutableArray * section1 = [[self class] stringsForSection:1 numberOfStrings:10];
    [self.dataModel addObject:section1];
}

// Utility.
+ (NSMutableArray *)stringsForSection:(NSUInteger)section numberOfStrings:(NSUInteger)numberOfStrings
{
    NSMutableArray * strings = [[NSMutableArray alloc] initWithCapacity:numberOfStrings];
    for (NSUInteger i = 0; i < numberOfStrings; ++i) {
        NSString * string = [NSString stringWithFormat:@"%d-%d", section, i];
        [strings addObject:string];
    }
    return strings;
}

- (void)loadView
{
    [super loadView];
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Add a table view.
    UITableView * tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
    self.tableView = tableView;
    self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.view addSubview:self.tableView];

    // Configure the table view.
    self.tableView.sectionHeaderHeight = 40;
    self.tableView.allowsSelection = NO;

    self.tableView.dataSource = self;
    self.tableView.delegate = self;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

// ---------------------

// Event Handers

- (void)pressedAddButton:(id)sender
{
    // Update data model: add a new string to the second section.
    NSUInteger section = 1;
    NSMutableArray * strings = [self.dataModel objectAtIndex:section];
    NSUInteger newStringIndex = strings.count;
    NSString * newString = [NSString stringWithFormat:@"1-%d (New String)", newStringIndex];
    [strings insertObject:newString atIndex:newStringIndex];

    // Update display: add a new row in the table for the newly added string.
    NSIndexPath * indexPath = [NSIndexPath indexPathForRow:newStringIndex inSection:section];
    [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];

    // Scroll to the newly added row.
    [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:YES];

    // After a brief pause, show that the newly added row is "special" by making its height taller than the other rows.
    double delayInSeconds = 0.1;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        // Update display model: set the special row to be the newly added row.
        self.indexPathOfSpecialRow = indexPath;

        // Update display: tell the table view to update the heights of the rows
        // (i.e., tableView:heightForRowAtIndexPath: will be called for each row).
        [self.tableView beginUpdates];
        [self.tableView endUpdates];
    });
}

// ---------------------

// Table View Data Source and Delegate Methods

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return self.dataModel.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSArray * strings = [self.dataModel objectAtIndex:section];

    return strings.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Data from the data model.
    NSArray * strings = [self.dataModel objectAtIndex:indexPath.section];
    NSString * string = [strings objectAtIndex:indexPath.row];

    static NSString * const kCellIdentifier = @"Cell";
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier];
    }

    cell.textLabel.text = string;

    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.indexPathOfSpecialRow != nil && [indexPath isEqual:self.indexPathOfSpecialRow]) {
        return tableView.rowHeight * 1.5;
    }
    else {
        return tableView.rowHeight;
    }
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    static NSString * const kHeaderIdentifier = @"Header";
    UITableViewHeaderFooterView * headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:kHeaderIdentifier];
    if (headerView == nil) {
        headerView = [[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:kHeaderIdentifier];

        // Give the header view a border (need to link to QuartzCore.framework to access layer property of UIView).
        headerView.contentView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.5].CGColor;
        headerView.contentView.layer.borderWidth = 1;
    }

    headerView.textLabel.text = [NSString stringWithFormat:@"Section %d", section];

    return headerView;
}

@end
like image 455
platypus Avatar asked Oct 26 '13 23:10

platypus


1 Answers

I know my answer is late, but I had the same problem. All system animations by IOS made by CATransaction. My solution is:

[CATransaction begin];

[CATransaction setCompletionBlock:^{
    //scroll view here
}];

[self.tableView beginUpdates];
[self.tableView endUpdates];

[CATransaction commit];
like image 95
oks_ios Avatar answered Sep 29 '22 05:09

oks_ios