views:

288

answers:

1

Consider a very simple UITableView with one of two states.

First state:

  • One (overall) table footer
  • One section containing two rows, a section header, and a section footer

Second state:

  • No table footer
  • One section containing four rows and no section header/footer

In both cases, each row is essentially one of four possible UITableViewCell objects, each containing its own UITextField. We don't even bother with reuse or caching, since we're only dealing with four known cells in this case. They've been created in an accompanying XIB, so we already have them all wired up and ready to go.

Now consider we want to toggle between the two states.

Sounds easy enough. Let's suppose our view controller's right bar button item provides the toggling support. We'll also track the current state with an ivar and enumeration.

To be explicit for a sec, here's how one might go from state 1 to 2. (Presume we handle the bar button item's title as well.) In short, we want to clear out our table's footer view, then insert the third and fourth rows. We batch this inside an update block like so:

// Brute forced references to the third and fourth rows in section 0
NSUInteger row02[] = {0, 2};
NSUInteger row03[] = {0, 3};

[self.tableView beginUpdates];
state = tableStateTwo; // 'internal' iVar, not a property
self.tableView.tableFooterView = nil;
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObjects:
    [NSIndexPath indexPathWithIndexes:row02 length:2],
    [NSIndexPath indexPathWithIndexes:row03 length:2], nil]
    withRowAnimation:UITableViewRowAnimationFade];  
[self.tableView endUpdates];

For the reverse, we want to reassign the table footer view (which, like the cells, is in the XIB ready and waiting), and remove the last two rows:

// Use row02 and row03 from earlier snippet

[self.tableView beginUpdates];  
state = tableStateOne;
self.tableView.tableFooterView = theTableFooterView;
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:
    [NSIndexPath indexPathWithIndexes:row02 length:2],
    [NSIndexPath indexPathWithIndexes:row03 length:2], nil]
    withRowAnimation:UITableViewRowAnimationFade];  
[self.tableView endUpdates];

Now, when the table asks for rows, it's very cut and dry. The first two cells are the same in both cases. Only the last two appear/disappear depending on the state. The state ivar is consulted when the Table View asks for things like number of rows in a section, height for header/footer in a section, or view for header/footer in a section.

This last bit is also where I'm running into trouble.

Using the above logic, section 0's header/footer does not disappear. Specifically, the footer stays below the inserted rows, but the header now overlays the topmost row. If we switch back to state one, the section footer is removed, but the section header remains.

How about using [self.tableView reloadData] then? Sure, why not. We take care not to use it inside the update block, per Apple's advisement, and simply add it after endUpdates.

This time, good news! The section 0 header/footer disappears. :)

However ...

Toggling back to state one results in a most exquisite mess! The section 0 header returns, only to overlay the first row once again (instead of appear above it). The section 0 footer is placed below the last row just fine, but the overall table footer - now reinstated - overlays the section footer. Waaaaaah … now what?

Just to be sure, let's toggle back to state two again. Yep, that looks fine. Coming back to state one? Yecccch.

I also tried sprinkling in a few other stunts like using reloadSections:withRowAnimation:, but that only serves to make things worse.

NSRange range = {0, 1};
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:range];

...

[self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationFade];

Case in point: If we invoke reloadSections... just before the end of the update block, changing to state two hides the first two rows from view, even though the space they would otherwise occupy remains. Switching back to state one returns section 0's header/footer to normal, but those first two rows remain invisible.

Case two: Moving reloadSections... to just after the update block but before reloadData results in all rows becoming invisible! (I refer to the row as being invisible because, during tracing, tableView:cellForRowAtIndexPath: is returning bona-fide cell objects for those rows.)

Case three: Moving reloadSections... after tableView:cellForRowAtIndexPath: brings us a bit closer, but the section 0 header/footer never returns when switching back to state one.

Hmm. Perhaps it's a faux pas using both reloadSections... and reloadData, based on what I'm seeing at trace-time, which brings us to:

Case four: Replacing reloadData with reloadSections... outright. All cells in state two disappear. All cells in state one remain missing as well (though the space is kept).

So much for that theory. :)

Tracing through the code, the cell and view objects, as well as the section heights, are all where they should be at the opportune times. They just aren't rendering sanely. (Update: The view heights are NOT the same, but I didn't change them either! See my posted answer for more info.)

So, how to crack this case? Clues welcome/appreciated!

A: 

I'm putting this in the answer section because it helps (partially?) explain what I'm seeing. It just doesn't explain why yet. :)

When I first assign a view to the table section header (in response to tableView:viewForHeaderInSection:) it's set just as I defined it in the XIB:

<UIView: 0x376620; frame = (0 0; 320 50); autoresize = RM+BM;
 layer = <CALayer: 0x376720>>

Some time after we change the cells around and start responding to tableView:viewForHeaderInSection: with nil for section 0, said view gets some CABasicAnimation love (the table is about to animate, after all) ... and the view frame and autoresize params change!

<UIView: 0x376620; frame = (0 0; 320 10); autoresize = W; 
 animations = { position=<CABasicAnimation: 0x360b30>;
 bounds=<CABasicAnimation: 0x360a30>; }; layer = <CALayer: 0x376720>>

If we then switch BACK to the first state, and return that very same view as the section header once again, we see a bit of debris at assign-time:

<UIView: 0x376620; frame = (0 0; 320 10); autoresize = W; 
 layer = <CALayer: 0x376720>>

Yep. The frame and autoresize are not reset!

So it would seem we have run in to an unintentional side-effect when effectively removing a view from a table's section header.

My knee-jerk reaction: "If you're going to mess with my UIView, please put things back the way you found them!" At this point I don't know if this is a realistic expectation, but it's the first one that comes to mind.

To mitigate, I suppose I'll have to reset the frame and autoresize each time. Hmm ... seems a bit messy, y'think? Perhaps there's a better way, or I'm committing a faux pas elsewhere.

Then again, this frame/autoresize adjustment doesn't seem to pose a problem for the overall table header/footer view, even when they're removed and later re-added. Just the section header/footer views. (Wait, a correction: I can't tell if it affects the table footer view because there's nothing below it to collide with.)

For now, I've embedded another view within each header/footer view. This view is identical in form to the parent view, but it doesn't get changed at animation-time. Then it's just a matter of invoking something akin to this for each header/footer view affected by the change in state:

- (void)fixViewAnimationCruft:(UIView *)theView {
  UIView *subview = [[theView subviews] objectAtIndex:0];
  theView.frame = subview.frame;
  theView.autoresizingMask = subview.autoresizingMask;
}

(Not the most original method name, but it will do for now.)

Joe D'Andrea