iOS Table View beginUpdates visual bug, black cells - ios

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.

Related

Animate insertion of cells at bottom of UITableView

I'm making a UIViewController to manage a messaging screen. I'm doing this using a UITableView and some custom cells.
To make things simpler, each cell contains:
Its chat "bubble" (a UIView subclass)
Its chat text (a UILabel)
A timestamp header label (which might be hidden)
A bottom footer label (for "Sending...", "Delivered", etc.; also might be hidden)
Because of performance concerns, I am not using auto-sizing of cell heights, but caching cell heights into an NSMutableDictionary.
When the user sends or receives a new message, I want the following to occur:
The current last message cell is reloaded, hiding its bottom label, if needed.
The new last message cell is appended at the bottom of the UITableView.
The UITableView is scrolled so that the new last cell is visible.
I can get it to where the end state of the screen is as desired, but the animations in between are really kinda funky. I have tried a whole lot of different approaches to get the animations to behave. Basically, it seems like some major reloading is happening, even though the only cell that could possibly change its height is the last cell (prior to the insertion of the new cell). Plus, I'd like to have the last cell simply "appear" in place without animation. If it does, it should be off-screen, and then I should be able to animate it on-screen.
Here's my current "user sent a new message" method:
- (IBAction)sendButtonPressed {
//Creation of the new message, into 'message' variable
[self.messages addObject:message];
int thisIndex = (int)self.messages.count - 1;
NSIndexPath *this = [NSIndexPath indexPathForRow:thisIndex inSection:0];
int prevIndex = (int)self.messages.count - 2;
NSIndexPath *prev = [NSIndexPath indexPathForRow:prevIndex inSection:0];
[self removeCachedHeightForIndex:prevIndex];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[prev] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView insertRowsAtIndexPaths:#[this] withRowAnimation:UITableViewRowAnimationTop];
[CATransaction setCompletionBlock:^{
[self scrollToBottomAnimated:YES];
}];
[self.tableView endUpdates];
}
This appears to reload the last several cells in the UITableView. Or, at least, every cell that is visible when this is called seems to be animated in some way. Only the cell at prev is actually changing in any way.
Longterm, I might pull out the header and footer labels into different cells, but is there a way to fix this animation glitch as-is?

scrollToItemAtIndexPath destroys UICollectionViewCell layout

I am creating a paginated UICollectionView that scrolls horizontally and whose cells are as big as the collection view so that only one cell is shown at a time. I also want to display the last item first when the collection view first appears, so that the other items are revealed from the left instead of the default where the next items come in from the right.
To do that, I call scrollToItemAtIndexPath: in my view controller's viewDidLayoutSubviews as suggested by this answer. If I put the call to scrollToItemAtIndexPath in viewWillAppear or viewDidLoad, the collection view does not at all scroll to the last item.
However, this call destroys the layout of my collection view cells. For example, if I don't call scrollToItemAtIndexPath:, the collection view (the white area below) looks like the left one--correctly laid out but showing the first item. If I call scrollToItemAtIndexPath:, the collection view does initially display the last item, but the layout is messed up like in the right (the date isn't even showing anymore).
What am I doing wrong?
More info:
I see this error both in iOS 7 and iOS 8.
I use size classes in Xcode 6.1.
The code for viewDidLayoutSubviews:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForItem:self.unit.readings.count - 1 inSection:0];
[self.readingCollectionView scrollToItemAtIndexPath:lastIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
}
Whenever I've had this issue and I have a constant size cell I've worked around it by just setting the contentOffset manually and ignoring the collectionView methods.
self.collectionView.contentOffset = (CGPoint){
.x = self.collectionView.contentSize.width - cell.width,
.y = 0,
};
I put the following in viewWillLayoutSubviews (also works in viewDidLayoutSubviews):
[self.readingCollectionView scrollToItemAtIndexPath:lastIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
[self.readingCollectionView reloadData];
A UI issue persists, however: when the view controller appears, it displays the first item, THEN immediately refreshes to display the last item instead.
To get around this seemingly unresolvable problem, I hacked the UI instead: display a "Loading" label before the view controller appears, and show the collection view in viewDidAppear.

Dynamically sized table in the cell of another table not resizing when reshowing view

I have a UITableView of cells where one cell contains a UITableView. Selecting that row that contains the table pushes a new view onto the screen with a list of items. The user selects a list of items and then presses 'back' to pop that view. The parent view looks at the number of items selected and the height of the cell is supposed to adjust to show all items selected.
What happens is that when the view is reshown, the listed items extends into the cell below. If I scroll that cell off the screen, then back to it, the cell is then the right height showing all items correctly.
I've tried several things such as putting code into viewWillAppear of the main table like [self.view setNeedsDisplay] and [self.view setNeedsLayout], but it doesn't work.
I can't seem to find a way to make that cell redraw to it's right size without scrolling the table once the view appears.
Is there some other method to force the redraw before it actually appears?
try:
NSIndexPath *theIndexPath = [NSIndexPath indexPathForRow:cellRow inSection:cellSection];
NSArray *theIndexPaths = [NSArray arrayWithObject:theIndexPath];
[table reloadRowsAtIndexPaths:theIndexPaths withRowAnimation:NO];
What are you currently using to redraw the cell? Just waiting for it to redraw itself when it goes off screen and back on? Or are you using something like [table reloadData];?

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.

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

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

Resources