I've created a set of classes to simplify managing table views. One of the things they do is create a diff for a section based on what data is updated and then apply the correct insertions, deletions and moves.
This is all working great for most of my app, except for two uncooperative table views. Call them table A and table B. I've found a fix that works for each individually, but I can't fix both at the same time. I've checked that the diffs are being correctly generated, but for some reason I get different animations in each.
Both table A and table B are simply inserting rows. There are no deletions or moves.
In the below code BREAK_TABLE_A causes:
Table A - All rows in section=0 row>=1 (not row=0) to shift to the bottom while re-animating in from the top. Very ugly
Table B - A nice insertion animation
BREAK_TABLE_B causes:
Table A - A nice insertion animation
Table B - No animation. Just like reloadData
Ultimately, I would actually prefer the method in BREAK_TABLE_B because this allows me to keep the same cells for the same data, which means I can perform animations on those individual cells using configureCell:.
The code is relatively straightforward:
UITableViewRowAnimation animation = UITableViewRowAnimationFade;
...
[self.tableView beginUpdates];
NSMutableSet *insertedPaths...;
NSMutableSet *deletedPaths...;
NSMutableSet *refreshPaths...; // Will contain any cell that was present before and after the update.
[self.tableView moveRowsAtIndexPaths:... // Not executed in these examples
...
[self.tableView insertRowsAtIndexPaths:insertedPaths.allObjects withRowAnimation:animation];
[self.tableView deleteRowsAtIndexPaths:deletedPaths.allObjects withRowAnimation:animation];
if (BREAK_TABLE_A){
[self.tableView reloadRowsAtIndexPaths:refreshPaths.allObjects withRowAnimation:UITableViewRowAnimationNone];
} else { //BREAK_TABLE_B - I would prefer this solution if possible
for (NSIndexPath *path in refreshPaths){
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:path];
if (cell){
NSUInteger index = [newSection.rows indexOfObjectPassingTest:^BOOL(LQTableViewRow * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
return [obj.rowIdentifier isEqual:oldSection.rows[path.row].rowIdentifier];
}];
LQTableViewRow *row = newSection.rows[index];
[self configureCell:cell section:newSection row:row];
}
}
}
... update the actual data source ...
[self.tableView endUpdates];
In order to fix this, I've tried wrapping the above code in [CATransaction begin/commit/flush] as well as [UIView begin/commitAnimations] blocks. CATransaction freezes the app with no errors (probably waiting for some other animation) when using table B. UIView causes both views (and all others in the app) to animate incorrectly (cells fade in and animate from bottom to top of their final positions, rather than from the top to the bottom of the tableview).
I've also tried to load the views with dummy data such that they are as similar to each other as possible with the same weird results.
Well, it turns out that the crash when using the CATransaction led me to the answer. I had accidentally been doing [CATransaction begin] twice in one of my cells, instead of committing the transaction. Fixing that fixed the animations.
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 wish to select all rows in UITableView for which I am using a for loop as per below code:
- (void)selectAllRows
{
for (int row = 0; row < [self.tableView numberOfRowsInSection:1]; row ++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:1];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
I couldn't find any direct delegates on NSIndexPath class or on UITableView controller to select all the rows on UITableView cell. This is of 'n' complexity, but could there be a better way to select all the cells?
This is the only way to select all the cells. The real question is why are you wanting to select all the cells? Are you selecting them to get UITableView delegate callbacks? Are you selecting them for UI purposes?
You can't reference all cells in your tableView as the tableView doesn't "have" all of them as it uses reuse/dequeueing.
If by "UI purposes" you mean solely for the visual effect as opposed to, you could loop
through all visible cells in your tableview and set the alpha to 0.5 or something similar, and if you want to fade them out and in as if they were selected/deselected you could do some [UIView animate....] calls on all of them. If you were to do something like this, you might want to consider disabling scrolling for the short period of time your animation is occurring, because if you are doing animation on visible cells as soon as you scroll any of the new cells wouldn't have that animation. Once you're done animating, in the completion block, you can then re-enable scrolling.
I'm using a UITableView with static cells for the settings view of my application.
One of the sections of this table contains two cells, the second one only being visible when a UISwitch contained in the first one is turned on.
So the UISwitch's target is this:
- (void)notificationSwitchChanged:(id)sender
{
UISwitch* switch = (UISwitch*)sender;
self.bNotify = switch.on;
[self.settingsTable reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationTop];
}
I implemented numberOfRowsInSection: this way:
(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
if (section != 1)
return [super tableView:tableView numberOfRowsInSection:section];
if (self.bNotify)
return 2;
return 1;
}
This "works", but the animation is faulty, making the first cell disappear or slide upwards halfway below the previous section or various stuff depending on the animation type I choose. The cleanest is UITableViewRowAnimationNone but is still not perfect.
If I scroll the section out of view and back it looks normal again.
EDIT: took a few screenshots of the problem:
I tried adding beginUpdates and endUpdates before and after reloadSections but it didn't change anything. Using reloadData instead of reloadSections works but there's no animation at all.
Is it because the table's cells are static or am I missing something?
I think this method should solve your problem.
[self.tableView insertSections:<#(NSIndexSet *)#> withRowAnimation:<#(UITableViewRowAnimation)#>];
Just update the data source (you dont have to because your are updating the section count depending on some flags) and call it.
Maybe you have to use this method instead.
[self.tableView insertRowsAtIndexPaths:<#(NSArray *)#> withRowAnimation:<#(UITableViewRowAnimation)#>];
I have a tableview with custom cells with dynamic cell heights depending on the cell content.
My problem is the following, when I ask, programmatically, in the viewDidLoad, to scroll to a given position it works, except for the last row. Sometime the row appears but not fully, and sometimes it even does not appear. In both cases I have to scroll manually to see the row.
Here is the code :
[self.tableView reloadData];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:aRow inSection:aSection];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES ];
Is this a bug of iOS ? any workaround ?
As so far i came to know that,
All the operations used to before the view is shown on the screen are initialized in the viewDidLoad all the UI objects, data objects can be allocated and initialized in this method.
All the operations data modifications, UI modifications made to view need to be done in viewDidAppear. Or even some operations can be done in viewWillAppear.
So for your issue, the UITableView scrolling must be done after the table is loaded on & shown on screen i.e., in viewDidAppear.
Also note that viewDidAppear & viewWillAppear will be called each time view is shown to user, so if you want to scroll the table only for the first instance you can have a flag in your header indicating the instance.
[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
NSIndexPath *rowIndexPath = [NSIndexPath indexPathForRow:3 inSection:0];
[self.tableView scrollToRowAtIndexPath:rowIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
});
I don't know exactly why, but I guess this approach works because when we add(???) rows and call [tableView reloadData] tableView has no time to update some internal counters (like row counter) and calling [tableView scrollToRowAtIndexPath ...] has no effect since there is no such row at that time (again, probably correct in case you add rows or set tableView's data for the first time). Calling
dispatch_async(dispatch_get_main_queue(), ^{
...
});
after [tableView reloadData] gives tableView enough time to update row counter and perform scroll to existing row.
Vishy's approach works just because it gives enough time but applicable only if you need to scroll exactly one time when screen is loaded. Moreover it requires ugly flag to check every time viewDid/WillAppear.
I have a table with shadows above the top and below the bottom cell (using Matt Gallagher's solution here: http://cocoawithlove.com/2009/08/adding-shadow-effects-to-uitableview.html). These are added in the layoutSubviews method of the UITableView class extension.
I dynamically add and delete cells below each main cell (these provide additional data) - let's call these "detail" cells. There is only one ever open at a time. When deleting the "detail cell" beneath the last main cell, as the animation begins, the shadow flicks upwards to the last cell (above the detail cell). It does this because the layoutSubview methods considers the last cell of the table to have changed the moment the animation for deleteRowsAtIndexPaths begins (rather than when the animation ends).
So, in essence, I need a way to keep the shadow below the detail cell as its being deleted. Not sure of the best way to do this. If the UITableView no longer considers that cell to be the last cell, then I am not sure even how to get the cell (since the UITableView gets the cell thus):
NSIndexPath *lastRow = [indexPathsForVisibleRows lastObject];
if ([lastRow section] == [self numberOfSections] - 1 &&
[lastRow row] == [self numberOfRowsInSection:[lastRow section]] - 1)
{
//adds shadow below it here
}
So even trapping the start of the animation is not much use if the UITableView still thinks the main cell above the "detail" cell is the "lastObject".
Thanks for any ideas.
Try this
[CATransaction begin];
[tableView beginUpdates];
//...
[CATransaction setCompletionBlock: ^{
// Code to be executed upon completion
}];
[tableView deleteRowsAtIndexPaths: indexPaths
withRowAnimation: UITableViewRowAnimationAutomatic];
[tableView endUpdates];
[CATransaction commit];
I am sure that you can easily achieve this by using a custom table view class instead of using dependencies from external frame work just inherit from the uitable view and add subviews to it.
But if you insist to keep it this way. take a reference in your own variable before deleting it.
Swift (the idea is the same, you can of course use this in obj-c):
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.tableView.scrollToRowAtIndexPath(indexPathes, withRowAnimation: UITableViewRowAnimation.None)
}, completion: { (Bool) -> Void in
// The logic you want to execute after the animation
})