Why is my tableview not scrolling to the requested indexPath? - ios

I have a tableView in my view and all works fine, however each custom cell (ToDoListCell) has a text field which I allow users to edit, updating their data. All good and well, my problem occurs whenever trying to scroll to a cell which is going to be hidden by the keyboard.
Note: I adjust the size of the table using the NSNotificationCenter observer methods as the Apple documentation recommends and I know this is not the problem.
- (void)scrollToSelectedCell {
NSIndexPath *currentIndexPath = [NSIndexPath indexPathForItem:0 inSection:0];
while (currentIndexPath.row < self.todos.count) {
ToDoListCell *cell = (ToDoListCell *)[self.table cellForRowAtIndexPath:currentIndexPath];
if (cell.titleField.isEditing == YES) {
[self.table scrollToRowAtIndexPath:currentIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
break;
}
currentIndexPath = [NSIndexPath indexPathForItem:currentIndexPath.row + 1 inSection:0];
}
}
Any idea why it does not scroll to any cell which is not currently on screen?
Thanks,
Ben

Just a guess, but probably because
ToDoListCell *cell = (ToDoListCell *)[self.table cellForRowAtIndexPath:currentIndexPath];
is returning nil causing cell.titleField.isEditing == YES to always fail. So the scroll command is never called. Table views only hold onto visible cells, so if you've resized your table view such that the cell being edited is no longer visible, you're going probably going to get nil back. You need to determine the index path of the cell being edited before resizing the table.

If you are firing the code in response to a notification listener, are you firing the notification from the UI thread?
UI animations will not perform unless executed on the main thread.
dispatch_async(dispatch_get_main_thread(), ^{
// send notification
});

Related

UITableView bouncing back to the top of a section when calling reloadRowsAtIndexPaths

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

Change sub view of all cells in UITableView

I'm creating an app which contains a screen that shows a table view with custom cells. Each cell contains two labels and a subview, which further contains other subviews. I'm handling the click event on the cell to hide/show the subviews within the subview in the cell. How can I make it so that when I click on a single cell, the subview of all the cells will change?
It is like the Stock application in iPhone (using iOS 7), here is a screenshot:
As in the image above, when you click on any of the green box, all the boxes change to reflect the same type of value.
Please let me know if this approach is fine, or how this can be implemented.
There are a couple ways of doing this. The first that comes to mind would be to handle the different states within the UITableViewCell subclass, and just reload the visible cells:
[self.tableView reloadRowsAtIndexPaths:[self.tableView indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationAutomatic];
If you're looking for more control over the process though, this process could also be achieved by changing the state future cells should load into, and then calling a method on every visible cell. This would provide you with an easy way to have complete control over how the contents of the cell update.
// Change flag for cell state then...
for (NSIndexPath *indexPath in [self.tableView indexPathsForVisibleRows]) {
if (condition) {
MyCellSubclass *cell = (MyCellSubclass *)[self.tableView cellForRowAtIndexPath:indexPath];
[cell someMethodWithArg:(id)state];
}
}
To do something as in Stock app you should handle two method cellForRowAtIndexPath: and click action method.
In cellForRowAtIndexPath: you should do the check which cell/button was pressed and display value base on it:
//Pseudo code
//cellForRowAtIndexPath
if (cellNo3Pressed)
{
//set up text with the right value.
}
else if (otherCell)
{
//set up text with the right value.
}
This will handle the cell which are not visible on the screen.
The next action method should handle nice animation on all of the visible cell:
NSArray *paths = [tableView indexPathsForVisibleRows];
for (NSIndexPath *path in paths)
{
UITableViewCell *cell = (UITableViewCell *)[self.tableView cellForRowAtIndexPath:path];
//Animate changes for cell
}

UICollectionView: activating, scrolling to, and assigning first responder to distant cell

I've got a text field inside of a UICollectionViewCell that can receive first-responder status. The cell currently isn't visible on-screen, and I want to scroll to the cell based off of a button hit from a UISegmentedControl. There's two segments to this control… and a hit to the second segment should scroll to the first cell in the 2nd section of the UICollectionView. After this happens, the cell should get selected programatically, and then the text field inside of that cell is supposed to get first responder status and bring up the keyboard.
What's happening now (inside my action method from a value change from the segmented control) is that a call to -[UICollectionView selectItemAtIndexPath:animated:scrollPosition:] isn't scrolling to it at all (and I'm using UICollectionViewScrollPositionTop; may as well be "…None"). If I thumb down the list manually, the cell is indeed selected (it gets a darker background color in that state), but the text field certainly doesn't have first responder status.
To fix the scroll problem, I've been able to ascertain the position of the cell in the list, and scroll to the cell's content offset (I've also used scrollRectToVisible here). Then I manually select it (as well as telling the delegate to fire its appropriate method as well, where the cell's text field gains first responder status).
- (void)directionSegmentedControlChanged:(UISegmentedControl *)sender {
NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:sender.selectedSegmentIndex];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:path];
[self.collectionView setContentOffset:attributes.frame.origin animated:YES];
[self.collectionView selectItemAtIndexPath:path animated:NO scrollPosition:UICollectionViewScrollPositionNone];
[self.collectionView.delegate collectionView:self.collectionView didSelectItemAtIndexPath:path];
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
BDKCollectionViewCell *cell = (BDKCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
[cell.textField becomeFirstResponder];
}
The problem here is that the cell as it's seen in -[collectionView:didSelectItemAtIndexPath:] is nil, because it's not in the visible cell set of the collection view when the method gets fired.
What's the best way to solve this? I've tried tossing my scrolling code inside of a [UIView animateWithDuration:animations:completion:] block, and assigned first responder upon completion there, but manually animating the collection view in this manner neglects to load any of the cells that should be scrolled past. Any ideas?
Update: many thanks to #Esker, who suggested I simply perform the "focus selection" action after a delay using Grand Central Dispatch. My solution ended up looking like this.
- (void)directionSegmentedControlChanged:(UISegmentedControl *)sender {
NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:sender.selectedSegmentIndex];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:path];
[self.collectionView setContentOffset:attributes.frame.origin animated:YES];
dispatch_time_t startAfter = dispatch_time(DISPATCH_TIME_NOW, 0.28 * NSEC_PER_SEC);
dispatch_after(startAfter, dispatch_get_main_queue(), ^{
[self.collectionView selectItemAtIndexPath:path animated:NO scrollPosition:UICollectionViewScrollPositionNone];
[self collectionView:self.collectionView didSelectItemAtIndexPath:path];
});
}
I had a similar challenge with a UITableView: scrolling to a cell that was not yet visible, and assigning first responder to a UITextField within the target cell once it was visible. Here's a simplified description of how I handle this. I imagine this approach could work with a UICollectionView, but I don't have much experience with collection views.
If the desired cell/text field is currently visible, immediately send it becomeFirstResponder, and scroll to the cell if desired.
Otherwise, set a property in your view controller or a similar class that indicates that a text field needs focus, and which one needs focus
Tell the table view/collection view to scroll to the desired index path
In collectionView:cellForItemAtIndexPath:, you could try to check that property to see if a text field at the given indexPath needs to get focus, and if so, send it becomeFirstResponder immediately, but I found this won't work if the cell is scrolling into view, presumably because at this point, when you're configuring the new cell, it's not yet actually in the view hierarchy. So I added a check, if becomeFirstResponder returns NO at this point, I try again after a delay:
dispatch_after(someDelay, dispatch_get_main_queue(), ^(void){
[self getFocus:textField];
});
The getFocus method will both send becomeFirstResponder to the text field and clear that property that tracks which text field needs focus.
My actual implementation is somewhat specialized for the view model associated with my table view, and encapsulated in a couple of classes and using some KVO, but I wanted to avoid that and focus on the minimum required logic in the description above.

UITableView animation glitch when deleting and inserting cells with varying heights

I am having a problem with the animation that UITableView provides when deleting and inserting a cell at the same time.
I have a list of cells lets call them questions. When one question is tapped it should add a cell beneath itself to display the answer to that question. If another answer is already being displayed that answer should be removed from the table.
The issue arises when the cell being inserted is very tall. If it is so tall that it's eventual bounds encroach into the space that the cell to be deleted takes up then during the animation we see the through the answer cell that we are deleting to see the cell that is being added
(see link to video of problem)
the is what my code looks like to move around the cells in the table view
[tableView beginUpdates];
if (deleteIndex) {
[tableView deleteRowsAtIndexPaths:#[deleteIndex] withRowAnimation:UITableViewRowAnimationTop];
}
if (addIndex) {
[tableView insertRowsAtIndexPaths:#[addIndex] withRowAnimation:UITableViewRowAnimationTop];
}
[tableView endUpdates];
I have tried
[tableView beginUpdates];
if (deleteIndex) {
[tableView deleteRowsAtIndexPaths:#[deleteIndex] withRowAnimation:UITableViewRowAnimationTop];
}
[tableView endUpdates];
//do stuff to update data source
[tableView beginUpdates];
if (addIndex) {
[tableView insertRowsAtIndexPaths:#[addIndex] withRowAnimation:UITableViewRowAnimationTop];
}
[tableView endUpdates];
But because there's no callback confirming that the table view did complete the first set update before starting the second block pretty much the same problem occurs. I know I could use perform selector with delay, but this seems like a bandaid.
Second I tried to encompass the animation in a block like this
[UIView animateWithDuration:0.0 animations:^{
[tableView beginUpdates];
if (deleteIndex) {
[tableView deleteRowsAtIndexPaths:#[deleteIndex] withRowAnimation:UITableViewRowAnimationTop];
}
[tableView endUpdates];
//do stuff to update data source
} completion:^(BOOL finished) {
[tableView beginUpdates];
if (addIndex) {
[tableView insertRowsAtIndexPaths:#[addIndex] withRowAnimation:UITableViewRowAnimationTop];
}
[tableView endUpdates];
}];
Again, because the completion block is fired after we call endUpdates not after the updates actually complete this does not resolve the use.
I also went into storyboard to be sure that clip subviews to bounds is selected for the cells. Again, this does not resolve the issue because we are not seeing a subview of the cell expand beyond it's expected height.
Looking closer at the animation by slowing it down it looks like apple inserts the cell to be added under the cells that won't be changed in the table and then moves the cells that will remain into their new positions. As a result the cell that was deleted becomes a transparent window where we see what they are actually doing under the hood.
The right approach would be to first remove the old answer and then after the animation is over add the new answer. There is a UITableViewDelegate method that is triggered after the cell animation is complete.
tableView:didEndDisplayingCell:forRowAtIndexPath:
Inserting the new answer row within this delegate method will result in the correct animation.
There are a few details to keep in mind- You'll need some logic to ensure that the correct cell height is returned and that the correct number of expected rows in the section is returned. Those data source methods are called after we remove our old answer in and again when we add the new one when we call
endUpdates
on the table view
Using anything other than UITableViewRowAnimationTop results in some strange animation behavior. This is because the content view of the cell is not what is being animated.
i solve same problem in my project by hide control like:
in ~cell.m have
(void)setSelected:(BOOL)selected animated:(BOOL)animated;
{
if(selected == NO)
{
self.selectedView.hidden = YES;
self.descriptionLabel.hidden = YES;
}
else
{
self.selectedView.hidden = NO;
self.descriptionLabel.hidden = NO;
}
}
may it still helpful

UItableview scrollToRowAtIndexPath not displaying last row correctly

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.

Resources