I've run into this really strange phenomenon that I can't quite figure out. I have a UITableViewController that manages a UITableView. Pretty simple. I also have a UISearchDisplayController for searching the contents of the table view. The searching functionality will be able to delete items of the content displayed by the table view. So if the user chooses to delete one of the items they found while searching, I want to not only reload the UISearchDisplayController's table view but also the UITableViewController's table view. When I do that, the sections of the regular table view pop out and display above the UISearchDisplayController. It's really quite strange. I think the best way to explain it is with an image:
If any of you know what could possibly be causing this problem or know a workaround, that would be fantastic.
UPDATED AGAIN
As it turns out, if a table's header is reloaded in the background it pops in front of the search controller no matter what.
I solved this by disabling the fetchedResultsController (setting it to nil) and letting it load lazily again when needed when the search disappears.
UPDATED - Original answer below
In my case I'm using two fetchedResultsControllers one for the main tableview and one for the search.
I discovered that preventing animations when adding the section headers prevents this bug. So while searchDisplayController.active I simply disable the animation of the section change. see code below.
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
if (!self.reordering) {
UITableView *myTableView = controller == __fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
UITableViewRowAnimation animation;
if (self.searchDisplayController.active) {
animation = UITableViewRowAnimationNone;
} else {
animation = UITableViewRowAnimationFade;
}
switch(type)
{
case NSFetchedResultsChangeInsert:
[myTableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:animation];
break;
case NSFetchedResultsChangeDelete:
[myTableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:animation];
break;
}
}
}
ORIGINAL ANSWER
The other answer doesn't actually work on it's own. The reason is, the header that is showing is not a header in the searchDisplayController's tableview. It's a header from the main tableview that for some reason is being added above the search table view in the view hierarchy.
I solved this problem by disabling updates to the main tableview while searchDisplayController.active = YES.
In my case I'm using a lazily loaded fetched results controller so I did it like this:
- (void)searchDisplayController:(UISearchDisplayController *)controller didHideSearchResultsTableView:(UITableView *)tableView {
[self.tableView reloadData];
}
- (void)searchDisplayController:(UISearchDisplayController *)controller willShowSearchResultsTableView:(UITableView *)tableView {
self.fetchedResultsController.delegate = nil;
self.fetchedResultsController = nil;
}
However, I still have the problem that if I want to reloadData on the main tableview so it is seen in the background, the section headers still float in front of the darkened area.
Does anyone have a better solution for this? It seems like a legitimate bug for viewForHeaderInSection and titleForHeaderInSection when data is reloaded while covered by a UISearchDisplayController.
The simplest answer for me is to try and override the search view so you can't see the background table. But that takes away from the "Appleness" of the app.
Our solution is to do the following. It has only been tested in iOS 7:
In viewForHeaderInSection, return nil if self.searchDisplayController.active is YES
In didHideSearchResultsTableView, call [self.tableView reloadData] to reload the headers when the search table disappears
I ran into this recently as well...the approach I decided on was to queue updates to the main tableView in a suspended serial dispatch queue until the the UISearchDisplayController hides the searchResultsTableView. I would probably consider this a bug as the section headers should not show through the main tableView if the searchResultsTableView has taken over that layer.
I solved this in iOS 7 by only reloading the visible rows in the underlying table.
- (void)searchDisplayController:(UISearchDisplayController *)controller didHideSearchResultsTableView:(UITableView *)tableView
{
[self.tableView reloadRowsAtIndexPaths:[self.tableView indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationNone];
}
Fixed it..
Add the following lines in viewDidLoad
searchDisplayController.searchResultsTableView.delegate = self;
searchDisplayController.searchResultsTableView.dataSource = self;
That fixed it for me...
[self.searchDisplayController.searchResultsTableView reloadData];
Because of using UITableViewController. self.view is a TableView in the UITableViewController and SearchDisplayController's ContainerView is added to the self.view of UITableViewController. Just use UIViewcontroller.
My solution was to avoid reloading the table if search results were displaying, then reloading any time the search results were dismissed.
I had to set a symbolic breakpoint on UITableView reloadData to find all the calls to reload that were causing the section headers to redraw on top of the search table.
Hopefully you've figured this out by now, but just in case someone stumbles across this question: this is probably happening because your UITableViewController is the data source/delegate for the search table as well as your main table. That is, presumably, you have the same UITableViewDelegate/DataSource methods executing for both table views, and you're returning the same section header for both tables. Make sure you're handling your search results table separately:
- (NSString *)tableView:(UITableView *)aTableView titleForHeaderInSection:(NSInteger)section {
if (aTableView == [[self searchDisplayController] searchResultsTableView]) {
return nil;
}
// Return a title appropriate for self.tableView here
}
Related
I have 3 or 2 sections (depending on datasource), in my grouped UITableView. I am trying to reload the last section via:
dispatch_async(dispatch_get_main_queue(), ^{
[UIView performWithoutAnimation:^{
[feedDetailTB reloadSections:[NSIndexSet indexSetWithIndex:feedDetailTB.numberOfSections-1] withRowAnimation:UITableViewRowAnimationNone];
}];
});
First of all, the footer never disappears. The data source basically keeps track of whether there are more comments or not (a simple load more functionality). In the viewForFooterInSection I simply return nil, when all the comments have been loaded.
But, as you see in the GIF, at first the loading button stays there. It is even accessible and works. When I scroll up, it vanishes and one can see it in the bottom, which is correct. But after all the comments have been reloaded, it should vanish, but sadly it stays there.
If I use reloadData it works fine. But I can't used it, since I have other sections, which I don't need to reload.
Second, there is a weird animation/flickering of the row items, even when I have used UITableViewRowAnimationNone. Not visible in the GIF
You should implement "isTheLastSection" according to your logic
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
if (isTheLastSection) {
return 40;
}
return 0;
}
In order to add new rows to a section, you must use the insertRowsAtIndexPaths rather than just adding new objects to data source and reloading a section.
Here's the code:
NSMutableArray *newCommentsIndexPath = [[NSMutableArray alloc] init];
for (NSInteger i = currentCount; i < (_postDetailDatasource.commentsFeedInfo.allCommentsArray.count + serverComments.count); i ++)
{
NSIndexPath *idxPath = [NSIndexPath indexPathForRow:i inSection:sectionNumber];
[newCommentsIndexPath addObject:idxPath];
}
[_postDetailDatasource.commentsFeedInfo.allCommentsArray addObjectsFromArray:serverComments];
[feedDetailTB beginUpdates];
[feedDetailTB insertRowsAtIndexPaths:newCommentsIndexPath withRowAnimation:UITableViewRowAnimationFade];
[feedDetailTB endUpdates];
I have an app with a UITableView which can delete cells using a row action. However, if I do two in quick succession, the app crashes with a BAD_EXEC.
The problem is clearly timing related. I'm asking the table to do something else before it's quite finished with the old. It could be the animations or it could be the removal of cells.
Either way, calling reloadData on the tableview before I start seems to fix it. There are two problems with this solution.
Firstly, reloadData interferes with some of the niceness of the usual row removal animations. It's no biggie but I'd prefer it with all animations intact.
Secondly, I still don't fully understand what's happening.
Can any one help me understand and/or suggest a better solution?
Here's the code...
-(void) rowActionPressedInIndexPath: (NSIndexPath *)indexPath timing:(Timing) doTaskWhen
{
[self.tableView reloadData]; // This is my current solution
[self.tableView beginUpdates];
ToDoTask *toDo = [self removeTaskFromTableViewAtIndexPath:indexPath];
toDo.timing = doTaskWhen; // Just some data model updating. Has no effect on anything else here.
[self.tableView endUpdates];
}
removeTaskFromTableView is mostly code to work out if I need to delete an entire section or a row. I've confirmed the app makes the right choice and the bug works either way so the only relevant line from the method is...
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
Edit: I have noticed that the standard delete row action provided by the system does not allow me to move that fast. There is an in-built delay which (presumably) prevents this exact problem.
-(void) rowActionPressedInIndexPath: (NSIndexPath *)indexPath timing:(Timing) doTaskWhen
{
// [self.tableView reloadData]; // This is my current solution
[self.tableView beginUpdates];
ToDoTask *toDo = [self removeTaskFromTableViewAtIndexPath:indexPath];
toDo.timing = doTaskWhen; // Just some data model updating. Has no effect on anything else here.
[self.tableView endUpdates];
}
Or try to reload table view in main thread.
In the Apple reminders App, and in the details screen of a remind,
when you switch on the control "Remind me at a location", a row "Location" is added (in fact a table view cell).
I would like to do the same in one of my application, when a switch control is actived 2 cells are added... how can i do this?
Thank you for your help
Check out this Project
https://github.com/singhson/Expandable-Collapsable-TableView
It may helps you.
in the method handling the change of your UISwitch you could either just reload the complete tableview using -(void)reloadData or (much nicer) use - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
You can use UISwitch as accessory view in one of your cells. However, you can face problems with reusing cells. If you need only one row with switch, you can simply add the UISwitch as a strong property in your TableViewController. You have to create it when initializing the controller:
self.locationSwitch = [[UISwitch alloc] init];
[self.locationSwitch addTarget:self action:#selector(handleSwitchValueChanged:) forControlEvents:UIControlEventValueChanged];
And then you can use it as an accessory view for your UITableViewCell:
cell.accessoryView = self.locationSwitch;
And you have to add (or remove) rows when switch value changes":
-(void)handleSwitchValueChanged:(id)sender
{
NSIndexPath* indexPath = // calculate your index path;
if(self.locationSwitch.on) {
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else {
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
And also remember to update your data source, so the value returned by tableView:numberOfRowsInSection is consistent with value of self.locationSwitch.
I have spent hours searching for the solution with out any luck. I am trying to delete a row (also deselect same row) programmatically. After row deletion call below, UITableViewDelgate methods get called expectedly and data source is updated but UITableView is not refreshed. deselectRowAtIndexPath call also does not work. I tried all kinds of scenarios as shown by commented lines.
Here is my code:
checkoutPerson is called as a result of observer listening for NSNotificationCenter messages.
- (void) checkoutPerson: (NSNumber*) personId {
Person *person = [_people objectForKey:personId];
if( person )
{
// Remove person from data source
int rowIndex = person.rowIndex;
S2Log(#"Deleting row number=%d", rowIndex);
[_allKeys removeObjectAtIndex:rowIndex];
[_people removeObjectForKey: personId];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:0];
//[[self tableView] beginUpdates];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
S2Log(#"Deleting indexPath row=%d", [indexPath row]);
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
//[[self tableView] endUpdates];
S2Log(#"Reloading data");
//[[self tableView] reloadData];
//[self performSelector:#selector(refreshView) withObject:nil afterDelay:1.5];
//[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES];
}
}
I will appreciate for help.
Thanks
-Virendra
I believe deleted cell is not being recycled. If I delete row in the middle, last row is always erased (since there is one less item) but the deleted row remains.
Use the above code between two function for table view
[tableView beginUpdates];
// the deletion code from data source and UITableView
[tableView endUpdates];
By calling this functions you are telling UITableView that you are about to make updates for deleting your cell.
Edit
The other problem I see with your code is you first delete the data from the data source.
Now you are asking for the UITableViewCell (which actually reloads the UITableView)
and then you are deleting the row from UITableView
I guess you should fetch the UITableViewCell before deleting values from your data source.
I found the problem. It has nothing to do with the code I posted above. It is syncing problem between visual display and the contents of data source. I have an embedded UITableView as part of a composite view. In composite view's controller, I was wiring up UITableView's delegate and data source to an instance of UITableViewController. Instead of this, I should have set UITableViewController's tableView property to the embedded UITableView. It seems that UITableView has to be contained within UITableViewController in order to correctly sync up table view visual display to the contents of data source. This also fixes row deselection and scrolling. I also needed to delay reloadData call in which case deleteRowsAtIndexPaths:withRowAnimation is not required. All you need is to modify the contents of your data source and call reloadData with a delay of 1.5 Seconds.
Thanks to all for great help.
I am trying to understand how UITableView accessibility works (in order to add the support to some other libraries that work in a similar way). So I implemented UIAccessibilityContainer Protocol in MyTableView subclass of UITableView.
First of all I need to render the dataSource accessible:
- (id<UITableViewDataSource>)dataSource
{
return (id<UITableViewDataSource>)[self valueForKey:#"_dataSource"];
}
Then I reimplement:
- (NSInteger)indexOfAccessibilityElement:(id)element
{
return [[self indexPathForCell:element] row];
}
- (NSInteger)accessibilityElementCount
{
return [[self dataSource] tableView:self numberOfRowsInSection:0];
}
and accessibility still works as supposed. Last step would be to implement - (id)accessibilityElementAtIndex:(NSInteger)index:
- (id)accessibilityElementAtIndex:(NSInteger)index
{
return [[self dataSource] tableView:self cellForRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
}
But apparently adding this method accessibility for the tableview stops working and I get just a VoiceOver "empty list" when navigating (with voiceover) to the tableview. Funny thing cells are returned properly and I get loads of AX ERROR: Could not find my mock parent, most likely I am stale.
I am trying to investigate, I'll post results unless someone comes up with ideas sooner.
Try to Implement those UIAccessibilityProtocal methods inside the UITableviewVell Class. Make the Cell accessible and check.