Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Frame of header view changed forever after deletion of UITableView's section

I'm reusing headers, keeping them in dictionary where section number is key and header view is value.

After I animated deleted one section and reload another one, header views of only these sections "disappeared". After some searching, I found that frames of these header view was changed. Frame.X of header view from section that was deleted with UITableViewRowAnimationRight shifter to right by screen width (320), and the one that was realoaded with UITableViewRowAnimationAutomatic shifted by Y.

The main problem and confusion comes from the fact that I'm unable to change these frames! Well, they change at first, but nothing changes visually and on the other call of viewForHeaderInSection: frames are shifted again.

Here is how I do that:

Assume we have 3 section, with data of each lays in one of arrays _first, _second and _third. And by button's touchupinside we call handleTouchUp:. (_second remains unchanged)

- (void) handleTouchUp: (UIButton *)sender
{
    if ([_first count] == 0) {
        sectionCount = 3;
        _first = [NSMutableArray arrayWithObjects: @"First 1", @"First 2", nil];
        _third = [NSMutableArray arrayWithObjects: @"Third 1", @"Third 2", @"Third 3", @"Third 4", @"Third 5", nil];
        [tableView reloadData];
    } else {
        sectionCount = 2;
        [_first removeAllObjects];
        _third = [NSMutableArray arrayWithObjects: @"First 1", @"First 2", @"Third 1", @"Third 2",
                                                   @"Third 3", @"Third 4", @"Third 5", nil];
        [tableView beginUpdates];
        [tableView deleteSections: [NSIndexSet indexSetWithIndex: 0] withRowAnimation:UITableViewRowAnimationRight];
        [tableView reloadSections: [NSIndexSet indexSetWithIndex: 2] withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView endUpdates];
    }
}


- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    NSNumber *key = [NSNumber numberWithInteger: section];
    UIView *header = [_headers objectForKey: key];
    if (header) {

    } else {
        header = [[UIView alloc] init];
        switch (section) {
            //Doing something with header here (e.g. set background color)
        }
        [_headers setObject:header forKey: key];
    }

    [header setFrame: CGRectMake(0, 0, 320, 25)];

    return header;
}

Here is full code of ViewController.m and test project

UPDATE: There may be some other errors in project, but it's not important -- it's test project. The only important problem is invalid header views. Removing invalid view and creating the new one isn't the solution -- it kills the point of reusing header views.

like image 886
folex Avatar asked Oct 22 '22 08:10

folex


1 Answers

I found several mistakes in your project which actually led to some crashes and I believe they were also responsible to the header view bugs you were seeing in the app. After I fixed those and the application seems to function properly to me.

Here is an updated version of your project: https://bitbucket.org/reydan/tableheaderviews

I tried to keep it as similar as possible to your initial project, but I had to make some changes. If you believe it does not match your desired functionality, leave a comment and I will update (This is what I deduced that the project should behave like) All changes that are explained below, can also be seen in this updated project.

The problems I found were related to the fact that if you delete a section from a table view, all the section after it will have their index changed (with -1). This is normal, because section index is really an index and not an unique key assigned to each section.

After you removed the first section , the third section now had index = 1 and the table view delegate methods read data incorrectly (including header views) since you had everything based on the section index.

To fix this, I changed the header view cache to use the data arrays as keys (_first, _second or _third). To make this easier, I created a method which can called from multiple places and returns the correct data array based on the section index and current section count:

- (NSArray*)dataArrayForSection:(NSInteger)section
{
    if (sectionCount == 3)
    {
        switch (section)
        {
            case 0: return _first;
            case 1: return _second;
            case 2: return _third;
        }
    }
    else
    {
        switch (section)
        {
            case 0: return _second;
            case 1: return _third;
        }
    }

    return nil;
}

Now that the header cache uses the data arrays as keys, it is important to remove the header entries when reallocating the arrays in the handleTouchUp method.

- (void) handleTouchUp: (UIButton *)sender
{
    NSLog(@"Touched!");
    if ([_first count] == 0)
    {
        sectionCount = 3;

        // **INSERTED** Removes the cache for the _first and _third keys because you are recreating the arrays again,
        // and so you are losing the initial key. Not doing this, results in a memory leak
        [_headers removeObjectForKey:_first];
        [_headers removeObjectForKey:_third];

        _first = [NSMutableArray arrayWithObjects: @"First 1", @"First 2", nil];
        _third = [NSMutableArray arrayWithObjects: @"Third 1", @"Third 2", @"Third 3", @"Third 4", @"Third 5", nil];

        [tableView reloadData];
//        [tableView beginUpdates];
//        [tableView reloadSections: [NSIndexSet indexSetWithIndex: 0] withRowAnimation:UITableViewRowAnimationRight];
//        [tableView reloadSections: [NSIndexSet indexSetWithIndex: 2] withRowAnimation:UITableViewRowAnimationAutomatic];
//        [tableView endUpdates];
    }
    else

Another thing I improved is reusing the table view cells. You should always try and reuse existing UITableViewCells (with few exceptions). The cellForRowAtIndexPath now looks like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSArray* dataarray = [self dataArrayForSection:indexPath.section];


    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"my_text_cell"];
    if (cell == nil)
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"my_text_cell"];


    if (dataarray == nil)
        cell.textLabel.text = @"default";
    else
        cell.textLabel.text = [dataarray objectAtIndex:indexPath.row];


    return cell;
}

If you have any questions, please leave a comment.

EDIT

I tried to update my test project as you requested, to keep the cache intact and I was indeed able to reproduce the bug - after the delete/reload animation, the header for the last section is not visible.

What I discovered is that the Automatic reload animation iOS chooses is the Fade animation. This causes the header view to fade out. I tested this by adding another button in the app that sets alpha=1 for all cached header views. After the reload animation, I pressed this button and the header views appeared correctly.

I tried using other animations, but they didn't look nice.

In the end, I don't think Apple recommends caching header views. In the documentation , starting with iOS 6.0 there is a new method that addresses this issue. It behaves similar to dequeueing cells, but for header/footer views
- (id)dequeueReusableHeaderFooterViewWithIdentifier:(NSString *)identifier

So, if you target iOS 6.0+ then you can use this instead of manually caching the headers. Otherwise, the solution I found is to remove from cache only the header views for the deleted and reloaded sections (_first and _third).

I updated the project to remove only the last section header when refreshing

like image 126
Andrei Stanescu Avatar answered Oct 31 '22 11:10

Andrei Stanescu