Trying to be ios7-esque, I am inserting a UIPickerView into a UITableView when tapping on a cell in the table. This works fine and animates nicely. However, there is an issue when I retract the cell by calling deleteRowsAtIndexPaths.
I am experiencing a "bleed"/overlap where the picker is hiding one of the cells further down in the table view. See the screenshots.
I'm not doing anything super custom, so I wonder if this is an iOS7 bug. All cells have solid background colors (white).
Any help would be greatly appreciated. Thanks!
Tapping the top row
This is mid animation when retracting. Notice the overlap and the picker bleeding out over the cell at the bottom
I'm not sure why, but it looks to me like the picker cell is covering the cell below "Choose Product". If this is indeed the case, one workaround would be to explicitly set the z-order of your cells, placing the picker cell under all others:
#import <QuartzCore/QuartzCore.h>
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = ...;// your logic for getting a cell
BOOL isPickerCell = ...;// your logic for identifying if this is the picker cell
cell.layer.zPosition = isPickerCell ? 0 : 1;
}
If the picker is near the bottom of the table, it could still show through below the last cell since there's nothing there to cover it. For example, if "Choose Product" were the last cell. You can work around this by inserting blank cell(s) at the bottom. This is a general problem with having cells of varying height.
After struggling with this problem, I realised that Apple's calendar application has the same issue
However, they minimise the side effects by inserting the row with a .fade animation.
tableView.insertRows(at: indexes, with: .fade)
I had similar issues (iOS 8, iPhone 6 simulator)
In my case, I had a custom cell containing a DatePicker being inserted/deleted either between Right Detail style cells or between a Right Detail style cell and the section footer, which worked as expected.
[self.table beginUpdates];
if (isVisible) {
[self.table insertRowsAtIndexPaths:#[index]
withRowAnimation:UITableViewRowAnimationMiddle];
} else {
[self.table deleteRowsAtIndexPaths:#[index]
withRowAnimation:UITableViewRowAnimationMiddle];
}
[self.table endUpdates];
But I also had Right Detail style cell being inserted/deleted between a Right Detail style cell and the end of section, which did not work as expected using the same code. The appearing/disappearing cell was visible on top of/through the cell above, and the cell moved twice as far as it should have. In the image below, People is appearing below Privacy, mid-animation.
However, I noticed that when the beginUpdates/endUpdates were commented out, the cell only moved about half a cell height instead of twice a cell height which meant that it looked much improved.
I also tried setting the zPosition which appeared to lessen the visibility when the cells overlapped.
// [self.table beginUpdates];
if (isVisible) {
[self.table insertRowsAtIndexPaths:#[index]
withRowAnimation:UITableViewRowAnimationMiddle];
} else {
cell = [self.table cellForRowAtIndexPath:peopleIndex];
cell.layer.zPosition = -1;
[self.table deleteRowsAtIndexPaths:#[peopleIndex]
withRowAnimation:UITableViewRowAnimationMiddle];
}
// [self.table endUpdates];
Related
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?
I have a list of custom cells in my tableview and as I scroll everything appears to fine and the cells appear to be in the same order. I have some functionality with my cells - as i select a cell (and it dynamically expands)the background color changes and a few other custom cell properties. Once I do this and then I start scrolling, different cells that i haven't even touched before show up, selected(expanded) and the cell only updates when I select it manually to the correct data. I seem to see duplicates and all kinds of craziness.
I know there are LOTS of posts about this on here so far but for me, so far nothing has worked. Would like some input on what I could do to stop this ridiculous behavior.
I have posted some code to give you a better idea of what I am doing. I know that 'dequeueReusableCellWithIdentifier' is the culprit but don't know of an alternative.
As side notes, this is a tableview(its own xib) that is a child view of a large view (also a xib). I have also already registered the nib for the tableview.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCustomCell *cell = [tableView dequeueReusableCellWithIdentifier:myIndentifier forIndexPath:indexPath];
if(self.currentSelectedIndex){
if(self.previousSelectedIndex){
//collapse cell
//configure cell in method(change background color etc)
}
else{
//expand cell
//configure cell in method(change background color etc)
}
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.currentSelectedIndex = indexPath;
[tableView beginUpdates];
if(self.currentSelectedIndex){
if(self.previousSelectedIndex && (self.previousSelectedIndex != self.currentSelectedIndex)){
[tableView reloadRowsAtIndexPaths:#[self.currentSelectedIndex, self.previousSelectedIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else{
[tableView reloadRowsAtIndexPaths:#[self.currentSelectedIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
[tableView endUpdates];
if(self.previousSelectedIndex == self.currentSelectedIndex){
self.previousSelectedIndex = nil;
}
else{
self.previousSelectedIndex = self.currentSelectedIndex;
}
}
What can I do or how would i make sure that nothing else in the list 'seems' to be selected(expanded) or prevent from appearing to see duplicates as i scroll? I already keep track of my current and last selected index(as shown in the code) so I suppose that I could use that somehow?
Dequeued Cells are Reused
Know that cells are re-used, so that the appearance of a UITableViewCell is persistent for the entire life of that cell.
This means that if you do not explicitly reset all the presentation view of your cell, and just returning it unchanged in cellForRowAtIndexPath, what you are returning may be a currently selected (or deselected) cached cell.
A possible location to reset a table cell is prepareForReuse.
Design note:
How are you maintaining self.currentSelectedIndex and self.previousSelectedIndex? This is typically quite dangerous, since you are attempting to replicate the UITableView behavior. It is for example, unlikely to work with multiple selection. Setting an active selection is unlikely handle situations when the OS didDeselectRowAtIndexPath, as a result of a keyboard dismissal for example.
When a user taps a button in one of my rows I am updating the underlying model for that row and then calling reloadRowsAtIndexPaths for the given row (i.e. single row reload).
- (IBAction)handleCompleteTouchEvent:(UIButton *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForView:sender];
id item = [self dataForIndexPath:indexPath];
if ([item respondsToSelector:#selector(completed)]) {
// toogle completed value
BOOL completed = ![[item valueForKey:#"completed"] boolValue];
[item setValue:[NSNumber numberWithBool:completed] forKey:#"completed"];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
}
}
The problem is that the table view bounces back to the top of the section after making this call. How can I prevent this from occurring and keep the scroll position where it is?
Ah Ha! I found the problem and am going to answer my own question for the poor soul who runs into this issue in the future.
All of my cells have variable height so I was using the new iOS7 method in UITableViewDelegate thinking it might speed up render time (not that I really needed it):
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath;
Anyway, implementing this method has the evil side effect of causing the table to bounce to the top of the section when calling:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
To solve the bounce problem I just removed the override of that estimatedHeightForRowAtIndexPath method and now everything works as it should. Happy at last.
This did the trick for me.
UIView.setAnimationsEnabled(false)
tableView.reloadRows(at: [...], with: .none)
UIView.setAnimationsEnabled(true)
Swift 4.2
This can work anywhere you want to remove animation.
During reload of table , table section or any row
UIView.performWithoutAnimation({
cell.configureSelection(isSelected: true)
tableView.reloadSections([1], with: .none)
tableView.allowsSelection = false
})
You should be able to do what you are trying to do by changing the cell contents directly. For example, if you are using the base UITableViewCell class and the data in your model is a NSString which you show in the table view cell, you can do the following (after you change your data model) instead of calling reloadRowsAtIndexPaths:
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
UILabel *label = [cell textLabel];
label.text = #"New Value";
If you are using a custom subclass of UITableViewCell, it's roughly the same except for accessing the views of the cell will need to be done through the contentView property.
It may be possible to put the cell in it's own section and call reload section:
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
this appears to fix the issue partially. Not the cleanest way but it may work for you.
In my similar case, I had to tweak the implementation of method
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
...
}
where my heights were being reset. So, instead of resetting height for every cell I updated only the effected cells for reloadRowsAtIndexPaths call.
If you know the minimum height of your cell, you must specify that while setting estimatedRowHeight. I was setting it to 1 as I have read before somewhere that any value above 0 would suffice the purpose, but it was the culprit.
When I set it to 44, which was the minimum height my cell could have, all went fine.
Dynamic heights were also working fine, no issues with this fix.
To build off xsee's answer -
I had set the Estimate in interface builder to "automatic". I changed this to another number and it started working. I kept Row Height to automatic.
I had the same issue. I ended up just calling tableView.reloadData() instead after updating my data / cell and it didn't bounce back to the top / data was updated in place - FYI
This solved this issue for me with Swift 5 / iOS 14:
UIView.performWithoutAnimation({
self.tableView.reloadRows(at: [self.idxTouched], with: .none)
})
I have a UITableView with custom cells in it. The cells expands when I tap on them. I am facing an issue when I tap on the cell which is half cut by the bottom screen edge of the device. When I tap on this cell, I want cell to move up and then expand to show the details but right now it just expands and I do not know if is expanded or not. I need to manually scroll the table view up to see it.
Below is my cell tapping code. I implemented the related cell expansion methods and cell is expanding properly.
- (void)cartTableView:(UITableView *)iTableView didSelectRowAtIndexPath:(NSIndexPath *)iIndexPath {
[self.temporaryCartTable scrollToRowAtIndexPath:iIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
[self expandOrCollapseCartCellAtIndex:iIndexPath];
}
- (void)expandOrCollapseCartCellAtIndex:(NSIndexPath *)iIndexPath {
self.selectedIndexPath = iIndexPath;
[self.temporaryCartTable beginUpdates];
[self.temporaryCartTable reloadRowsAtIndexPaths:#[iIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.temporaryCartTable endUpdates];
}
After expanding, call scrollToRowAtIndexPath, which should position your cell correctly.
You need to check if rect of the cell you're extending (rectForRowAtIndexPath:) is fully within the bounds of the table view (based on contentSize and contentOffset, using CGRectContainsRect and if it's not - and only partially visible - then you need to scroll it to the bottom/top of the table view.
I have a custom tableFooterView (it's just an 8px high CoreGraphics arc with a gradient) that I set with the tableFooterView property in viewDidLoad rather than viewForFooterInSection. When setting it with viewForFooterInSection, it floated over the content when it reached the bottom, whereas tableFooterView does what I want it to in that it stays with the UITableView's height.
But when the cells or table view do change in height, the tableFooterView animates to them slowly (about half a second but it's very noticeable). This is pretty awkward since the footer is supposed to look like an extension of the last cell. For instance, when heightForRowAtIndexPath changes the height of a cell, the tableFooterView kind of ghost-floats back. In this screenshot the bottom cell has just been shrunken to its normal size and the footer is floating back.
(As a new user I can't post images but here's the link: http://desmond.imageshack.us/Himg835/scaled.php?server=835&filename=iossimulatorscreenshotj.png&res=landing)
(This content is no longer available, 14/9/15).
It will also float over the content when the height of the last cell is suddenly changed to be larger than it was.
Any pointers? Thanks very much.
Edit: By cells changing in height, I mean through the heightForRowAtIndexPath section:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
Note *currentNote = [self.notes objectAtIndex:indexPath.row];
if (currentNote.associatedCellIsSelected) {
return NORMAL_CELL_FINISHING_HEIGHT*2;
}
return NORMAL_CELL_FINISHING_HEIGHT;
}
Edit 2: In didSelectRowAtIndexPath I make the cell selected (actually the cell's note), begin / end updates as well as call reloadRow for the row that's been selected.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
Note *currentNote = [self.notes objectAtIndex:indexPath.row];
if (currentNote.associatedCellIsSelected == FALSE) {
currentNote.associatedCellIsSelected = TRUE;
[tableView beginUpdates];
[tableView endUpdates];
}
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; }
I've also made sure that I get the same behavior with a plain rect redColor UIView in place of the Core Graphics footer, with the same results. I wish there was just an easy way to override the footer and tell it to not animate!