UITableView: Juxtaposing row, header, and footer insertions/deletions - ios

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!

I had the same issue today.
After a while I came up with the idea that there is an error in the way everything is draw. In the appearance of table view cells method I removed the condition clause like this, everything worked :
//if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
//}
I'll have a closer look at the problem as the code is not optimized anymore now. But it works for the time being.

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.)

Related

Expand UILabel inside UITableView with "more" button like Instagram

I've got a project (that has been written by other people) where there's a feed with content and text displayed inside a table view. Each post corresponds to one section in table view, and each section has its own rows corresponding to elements like content, text, like button etc.
I need to display a short label for post captions with a "more" button inside table view cell, and when more button is tapped, the label will expand to whatever size the caption fits, all happening inside a table view cell. When the more button is tapped I change the label's numberOfLines property to zero, and as the cells have automatic height, all I need is to reload that particular caption cell. (the cell displays correctly with the expanded size if I set numberOfLines to 0 at the first place before displaying the cell.)
I've tried:
[tableView beginUpdates];
tableView endUpdates];
I've tried various animation options with:
[tableView reloadRowsAtIndexPaths:#[myPath] withRowAnimation:UITableViewRowAnimation(Bottom,Top,None etc)];
I've tried:
[tableView reloadSections:[NSIndexSet indexSetWithIndex:myPath.section] withRowAnimation:UITableViewRowAnimation(Top,Bottom,None etc)];
But they all yield the same result: the whole table view layout gets messed up: it jumps to another cell, some views go blank, cells overlap each other, video inside cells stop playing, and the label doesn't expand (but refreshes inside itself, e.g. that short preview txt with one line animates from top/bottom etc but doesn't expand).
What might be causing the mess up of the whole table view and how I can reload just one cell correctly with and expansion animation, without messing up the whole layout? I've seen many questions and answers regarding this, but they all recommend the options that I've already tried and explained above.
My app targets iOS 8.0+
UPDATE: Here is the relevant code (with some parts regarding inner workings that aren't related to layout, removed):
MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier: MyCellIdentifier forIndexPath:indexPath];
cell.delegate = self;
cell.indexPathToReloadOnAnimation = indexPath;
cell.shouldShortenCaption = YES;
id post = self.posts[indexPath.section] ;
[cell setPost:post];
return cell;
And inside setPost::
if(self.shouldShortenCaption){
captionLabel.numberOfLines = 2;
}else{
captionLabel.numberOfLines = 0;
}
NSString *text = [some calculated (deterministic) text from post object];
Button action is simple:
self.shouldShortenCaption = NO;
[one of the reload codes that I've written above in the question]
UPDATE 2: Here are some more methods regarding the issue:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (section < self.posts.count) {
return 59;
}else
return 0;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
if (section < self.posts.count) {
MyFeedHeader *header = [tableView dequeueReusableCellWithIdentifier:MyFeedHeaderIdentifier];
header.delegate = self;
[header setPostIndex:section];
[header setPost:self.posts[section]] ;
return header;
}
else
return nil;
}
Header's setPost: method basically sets the relevant texts to labels (which have nothing to do with the caption, they are completely different cells. the problematic cell is not the header cell). The table doesn't have any footer methods. The only method regarding height is the one above.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section >= self.posts.count) {
return 1;
}
id post = self.posts[section];
[calculate number of rows, which is deterministic]
return [number of rows];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
BOOL hasCursor = self.cursor && self.hasMore ? 1 : 0;
return self.posts.count + hasCursor;
}
(Post count and cursor and hasMore are also deterministic).
UPDATE 3: I've asked a question (which wasn't a duplicate, even though there are similar questions) and got a useful answer that solved my problem. Can the downvoters please elaborate the reason that they've downvoted?
Here is an example: https://github.com/DonMag/DynamicCellHeight
Table B is one way of accomplishing "More/Less" (Table A was for another layout I played around with). It uses the [tableView beginUpdates]; tableView endUpdates]; method of triggering the table re-layout.
The key is getting all your constraints set up correctly, so the Auto-Layout engine does what you expect.
The example is in Swift, but should be really easily translated back to Obj-C (I think I did it in Obj-C first).
Edit: some additional notes...
This is using a pretty standard method of dynamic-height table view cells. The vertical spacing constraints between elements effectively "pushes out" the bounds of the cell. The tap here toggles the numberOfLines property of the label between 2 and 0 (zero meaning as many lines as necessary). Sequential calls to beginUpdates / endUpdates on the table view tells the auto-layout engine to recalculate the row heights without needing to reload the data.
For this example, I did use a little "trickery" to get the smooth expand/collapse effect... The multiline label you see here is contained in a UIView (with clipsToBounds true). There is a second, duplicate multiline label (alpha 0 so it's not visible) that is controlling the height. I found that changing the numberOfLines on the visible label sort of "snapped" to 2 lines, and then the size change animation took place... resulting in the text "jumping around."
Just for the heck of it, I added the "not so good" version to my GitHub repo for comparison's sake.

After reloaddata in tableview stay in the same position

The user will click a button in one of the cell and the table will reload the data.
I would like to let the user stay in the same position after the reload.
My table contain many section and each section has two row.
Each of them may have different height.
I have tried some of the methods provided by others but its doesn't work.
CGPoint offset = tableView.contentOffset;
[tableView reloadData];
[tableView setContentOffset:offset];
Is it related to my table that each row in each section has different heigt?
I have also check that the offset is correct after the table is reloaded.
But the appearance is wrong.
I found that the view of the table is not change after the setting the offset

TableView sections become hidden after reloadSections

My question is the following: there is a table consisting of headers. When you click on the header, it restarts after
[self.tableView reloadSections: [NSIndexSet indexSetWithIndex: section] withRowAnimation: UITableViewRowAnimationAutomatic];
Actually, in the earlier version (iOS 6.1.3 and lower), cell reloaded and everything is fine. On the iOS7 cells become hidden. What is it and how to solve it? And not only cell that is clicked, but the next standing(lower or above).
<MyHeaderCell: 0x146ea600; baseClass = UITableViewCell; frame = (0 0, 320 75); hidden = YES; autoresize = W; gestureRecognizers = <NSArray: 0x146e10c0>; layer = <CALayer: 0x146e95d0 »
Basically, it is one of those issues in the logs. It is clearly seen that cells become hidden.
Nevertheless, when I use [self.tableView reloadData]; everything is fine, but I need animation.
this is an old question, but I think I know what's causing this.
Is this a "static" cell in the sense that you're keeping a reference to it yourself? In that case, the problem is probably this:
When you do an animated reload, the table view will fade out existing cells while at the same time fading in the new cells. The problem is that when the "new" cell is the exact same cell as the old one, the same cell will both fade in and fade out at the same time! And in your case, the fade out animation takes precedence and the cell is hidden.
This if fixed by always reusing cells instead of using the same reference, however, I know that this is not always optimal.
Calling [tableView reloadData] right after [tableView reloadSections:withRowAnimation] fixes it without significantly affecting the animation.

iOS Table View beginUpdates visual bug, black cells

I have a bug that I've spent way too much time on myself. Basically, I'm creating an "accordion" Table View based off Apple docs:
https://developer.apple.com/library/ios/#samplecode/TableViewUpdates/Listings/ReadMe_txt.html#//apple_ref/doc/uid/DTS40010139-ReadMe_txt-DontLinkElementID_3
The way the code works is that if you tap on a Table View Header, code gets called to add rows to that section.
The only thing that's different is the fact that it's for iOS 5, so for my header views I'm not using the reusable header view stuff.
Basically, every time I click on a header to show my rows, the cells that are added/removed via [tableView beginUpdates]/[tableView endUpdates] turn black. HOWEVER, if I scroll the table view after then it goes back to being normal.
Pictures showing this in action: http://imgur.com/a/B3lMu
Some code of how I remove cells :
// GET THE SECTION INFO
SWDeviceSectionInfo *deviceSectionInfo = [self.deviceTableSectionInfo objectAtIndex:section];
// "CLOSE" THE SECTION
deviceSectionInfo.open = NO;
// CREATE INDEX PATHS FOR ROWS THAT NEED TO BE DELETED
NSMutableArray *indexPathsToDelete = [[NSMutableArray alloc] init];
for (int i=0;i<deviceSectionInfo.rowInfo.count;i++) {
// CREATE INDEX PATHS FOR EACH ROW
[indexPathsToDelete addObject:[NSIndexPath indexPathForRow:i inSection:section]];
}
// APPLY THE ROW INSERTIONS
[self.deviceTableView beginUpdates];
[self.deviceTableView deleteRowsAtIndexPaths:indexPathsToDelete withRowAnimation:UITableViewRowAnimationBottom];
[self.deviceTableView endUpdates];
Some extra notes:
The background for the cells after scrolling table view is that of the Table View (I can't set the BG for cells). The Yellow view is a view on TOP of the cell background that's supposed to be green.
If I click on the iOS simulator debug "color" then again, the black cells disappear.
I've tried changing "highlighted", "selected" color to random colors and nothing changes. Remains a black cell bug.
I've triple checked the code for stuff like adding cells to the wrong section, all of that is seemingly correct.
The bug also occurred when I was using the default white cells (no custom nib).
The problem was that I had a view on top of the table.
To be more specific, the view had clear color background and shadows added to it so it would form an "inner shadow."
That view SOMEHOW caused the table to render completely wrong when it got updated.

Why is my UILabel in a UIView in a UITableCellView not updating after a correct database call, (it seems cell reuse related)?

I have a UITableview cell that gets a tally from a core data database. The tallyTable is in a view controller inside a UITab view. I have an NSLog statement that prints out the tally value whenever it gets updated. Another tab has a list to change the source (different day) for the tallies. I am using iOS5 with ARC targeting iOS 4.2.
Here's the problem. When I load the application, the correct tallies for whatever the last selected day show up in the table tab. If I then go to the day tab and change the day and return to the tally tab there is no change in the display. However, the viewWillAppear on the tally tab runs and as the table cycles through cellForIndexPath, my NSLog statement prints out all the correct new values. If I then scroll the top label off the screen and back the label updates to the new value.
I've tried setNeedsLayout and setNeedsDisplay on the UILabel, the UITableViewCell, the UITableView and the view controller loading the table. I tried changing the CellReuse identifier so that it would never reuse a cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CollectionItemTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[CollectionItemTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSUInteger row = [indexPath row];
cell.textLabel.text = [[self.collectionKeys objectAtIndex:row] valueForKey:#"collectionTitle"];
NSInteger test1 = indexPath.row + 150;
NSLog(#"tag = %i", test1);
cell.tallyButton.tag = test1;
NSNumber * questionID = [[self.collectionKeys objectAtIndex:row] valueForKey:#"answerID"];
cell.tallyLabel.text = [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]];
NSLog(#"Collection text should be = %#", [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]]);
[cell setNeedsLayout];
return cell;
}
I've read over a half dozen other similar questions. Got about three hours invested so far in trying to solve this.
EDIT: I thought I fixed it by using the navigation controller to repush the top level view controller on to the view again. I'll admit now this feels like a classically kludgy hack in every way. When the view is PUSHED everything updates and it is seamless. However, in order to have a fixed footer to make selection settings for the table buttons, I used a UIView with two subviews, a UITableView on top and a simple UIView with four buttons below.
The captions on the buttons need to change with the data source. Now when the view controller is pushed onto the view it obscures my fixed footer view. So, I inserted the fixed footer into the UITableview and everything appeared fine until I scrolled the UITableView and the footer scrolled up with it. The table is basically a tally sheet with buttons next to each item and in the footer is four buttons to note the color of the tallied item. Say the next item was a green lego, you would tap "green" in the footer and the button next to "lego" in the table. When I push the view controller with the two subviews the UITableview labels do not update. Thus the tableview needs to be pushed itself (as far as I can tell).
ANSWER: see comment below but ultimately I needed to reload both the visible UITableView data and the delegate UITableView controller data behind it.
I'll give it a shot. First, are you using ARC? If not, you need to add autorelease when you alloc/init a new cell. Otherwise, it's fine as is.
If I'm understanding your question correctly:
The tableView displays the correct data at app launch
You switch away from the tab with the tableView and change the tableView dataSource
You switch back to the tab with the tableView and you see (via NSLog) that the table cells are reloaded with the correct data yet the old data is still visible in the cells
If you scroll a cell off the display and back forcing it to refresh it contains the correct data
Some thoughts:
the tableView will not reload itself automatically when it's view appears. You need to call [tableView reloadData] whenever the dataSource changes. This is independent of whether the tableView is currently displayed or not. My guess is this alone will solve your problem.
You don't need to call setNeedsLayout on the cell unless you want the cell to relayout its subviews based on the data. You also don't need setNeedsDisplay.
I'm assuming there aren't other complicating factors (such as multiple tableViews displaying the same data) that could confuse things.
If you use prepare for reuse method, remember to over the original method with [super prepareForReuse];
Another method if the above way does not work is re setup cell as describe here.
I use the same method i applied for some of my collection view : we should remove/reset your subview where you create/add it to cell 's content. That mean we need set up data each cell completely for each row.
I move the code reset data value from prepare reuse to where i set value and I worked simply !
In my CustomCell.m :
- (void)configCellWith:(id)item At:(NSUInteger)row {
if (_scrollView) {
[[_scrollView subviews]
makeObjectsPerformSelector:#selector(removeFromSuperview)];
_scrollView = nil;
[_scrollView removeFromSuperview];
}
else {
CGFloat y = labelHeight+15;
float scrollHeight = _imgArray.count*200;
_scrollView=[[UIScrollView alloc]initWithFrame:CGRectMake(10, y,SCREEN_WIDTH-20, scrollHeight)];
_scrollView.scrollEnabled=YES;
_scrollView.userInteractionEnabled=YES;
_scrollView.layer.masksToBounds = YES;
[self.contentView addSubview:_scrollView]; } }
Remember to change your data source appropriately too.

Resources