问题
I'm trying to figure out the most elegant way of keeping row selection in sync between a UITableViewController
and a UISearchDisplayController
.
When a row is selected in the UITableViewController
I want the same row to appear as selected when the UISearchDisplayController
is active
, and vice versa.
Both tableView
objects have allowsMultipleSelection
set to YES
.
回答1:
The basis of this technique was gleaned from Erica Sadun's very helpful book "The Core iOS 6 Developer's Cookbook" fourth edition, published by Addison Wesley. I have developed many solutions from the ideas and code presented in Erica's book.
Notes
- This is not elegant but it does work.
- This solution is for a target running iOS 7 and below.
UISearchDisplayController
is deprecated in iOS 8 in favour ofUISearchController
. - To attempt to keep the answer as short as possible, this solution leaves out chunks of code necessary to properly prepare both
self.tableView
andself.searchDisplayController.searchResultsTableView
table views. - This solution assumes use of functioning Core Data Stack and
NSFetchedResultsController
.
Essentially we use an NSMutableDictionary
to maintain a record of selected cell/s that is used by both self.tableView
and self.searchDisplayController.searchResultsTableView
.
This technique can be used for table views and controllers that are intended to register and track one or more selected cells.
I may miss a few steps as it has been a while since I implemented this solution, so let me know and I will check it.
Step One
Prepare a set of public properties, including...
@property (nonatomic, retain) NSMutableArray *searchResults;
@property (nonatomic, strong) NSManagedObjectID *managedObjectID;
@property (nonatomic, strong) NSArray *arrayObjects;
@property (nonatomic) BOOL isArray;
@property (nonatomic, strong) void (^blockSelectManagedObjectID)(NSManagedObjectID *objectID);
@property (nonatomic, strong) void (^blockSelectManagedObjects)(NSArray *objects);
The properties managedObjectID
and arrayObjects
are set using a prepareForSegue
method contained within the parent TVC. Only one or the other is set, depending on whether you are passing one NSManagedObjectID
(single selection), or an NSArray
of multiple NSManagedObject
s (multiple selections).
The property isArray
could be removed, but I include it for my ease of coding and code readability. It is also set in the same prepareForSegue
method within the parent TVC mentioned above.
The blocks are defined in the parent TVC and update data in the parent TVC when exiting this TVC.
To summarise, apart from searchResults
, these public properties are set by the parent TVC.
Step Two
Prepare a set of private properties, including...
@property (nonatomic, strong) NSMutableDictionary *dictionaryTableRowCheckedState; //our primary dictionary!
@property (nonatomic, strong) NSMutableArray *arrayObjectsSelected;
@property (nonatomic, strong) NSIndexPath *indexPathSelected;
@property (nonatomic, strong) NSIndexPath *indexPathObjectFromArray;
@property (nonatomic, strong) NSManagedObjectID *cellObjectID;
Step Three
Set your private properties in the TVC lifecycle method viewWillAppear
.
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self setDictionaryTableRowCheckedState:[NSMutableDictionary dictionary]];
[self setArrayObjectsSelected:[NSMutableArray arrayWithArray:self.arrayObjects]];
[self setIndexPathSelected:nil];
[self setIndexPathObjectFromArray:nil];
[self.tableView reloadData];
//<<_YOUR_OTHER_CODE_>>
}
Step Four
Prepare to populate your UITableViewCell
s using the TVC data source method cellForRowAtIndexPath
, as follows...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//<<_YOUR_OTHER_CODE_>>
//<<including...>>
if (tableView == self.tableView) {
rowEntity = [self.fetchedResultsController objectAtIndexPath:indexPath];
} else {
rowEntity = [self.searchResults objectAtIndex:indexPath.row];
}
[self setCellObjectID:[rowEntity objectID]];
//<<_YOUR_OTHER_CODE_>>
[cell setAccessoryType:UITableViewCellAccessoryNone];
NSIndexPath *indexPathLastManagedObject = nil;
// If there exists 'checked' value/s, manage row checked state
if (self.managedObjectID || self.arrayObjects.count) {
BOOL isChecked = NO;
if (!self.isArray) {
if (self.cellObjectID == self.managedObjectID) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
isChecked = YES;
self.indexPathSelected = indexPath;
NSManagedObject *localManagedObject = nil;
if (tableView == self.tableView) {
localManagedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
} else {
localManagedObject = [self.searchResults objectAtIndex:indexPath.row];
}
indexPathLastManagedObject = indexPath;
self.managedObjectID = localManagedObject.objectID;
}
} else if (self.isArray) {
if (self.arrayObjectsSelected.count) {
for (NSManagedObject *localManagedObject in self.arrayObjectsSelected) {
if (self.cellObjectID == localManagedObject.objectID) {
isChecked = YES;
indexPathLastManagedObject = indexPath;
break;
}
}
}
self.dictionaryTableRowCheckedState[indexPath] = @(isChecked);
cell.accessoryType = isChecked ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
} else {
NSLog(@"%@ - %@ - has (possible undefined) E~R~R~O~R attempting to set UITableViewCellAccessory at indexPath: %@_", NSStringFromClass(self.class), NSStringFromSelector(_cmd), indexPath);
}
}
return cell;
}
Step Five
Of course we need a fairly chunky TVC delegate method didSelectRowAtIndexPath
to handle the selection and deselection of cells by the user.
Notice that I highlight the use of block callbacks in my code, although these have not been mentioned in detail - it is my method of choice to update data in the parent TVCs. If you'd like me to update the code to incorporate block callbacks let me know (just a lot of code here already).
Notice also that I pop the TVC when we are in single selection mode. If we are in multiple cell selection mode, the return to the parent TVC must obviously be manual.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (!self.isArray) {
if (self.indexPathSelected) {
if ((indexPath.section == self.indexPathSelected.section)
&& (indexPath.row == self.indexPathSelected.row)) {
[cell setAccessoryType:UITableViewCellAccessoryNone];
[self setIndexPathSelected:nil];
} else {
NSIndexPath *oldIndexPath = self.indexPathSelected;
UITableViewCell *oldCell = [tableView cellForRowAtIndexPath:oldIndexPath];
[oldCell setAccessoryType:UITableViewCellAccessoryNone];
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
[self setIndexPathSelected:indexPath];
NSManagedObject *localManagedObject = nil;
if (tableView == self.tableView) {
localManagedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
} else {
localManagedObject = [self.searchResults objectAtIndex:indexPath.row];
}
NSManagedObjectID *localObjectID = localManagedObject.objectID;
[self blockSelectManagedObjectID](localObjectID); //block callback to update parent TVC
}
} else {
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
[self setIndexPathSelected:indexPath];
NSManagedObject *localManagedObject = nil;
if (tableView == self.tableView) {
localManagedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
} else {
localManagedObject = [self.searchResults objectAtIndex:indexPath.row];
}
NSManagedObjectID *localObjectID = localManagedObject.objectID;
[self blockSelectManagedObjectID](localObjectID); //block callback to update parent TVC
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[self.navigationController popViewControllerAnimated:YES];
} else if (self.isArray) {
NSManagedObject *localManagedObject = nil;
if (tableView == self.tableView) {
localManagedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
} else {
localManagedObject = [self.searchResults objectAtIndex:indexPath.row];
}
// Toggle the cell checked state
__block BOOL isChecked = !((NSNumber *)self.dictionaryTableRowCheckedState[indexPath]).boolValue;
self.dictionaryTableRowCheckedState[indexPath] = @(isChecked);
[cell setAccessoryType:isChecked ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone];
if (isChecked) {
[self.arrayObjectsSelected addObject:localManagedObject];
} else {
[self.arrayObjectsSelected removeObject:localManagedObject];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
} else {
NSLog(@"%@ - %@ - has (possible undefined) E~R~R~O~R at indexPath: %@_", NSStringFromClass(self.class), NSStringFromSelector(_cmd), indexPath);
}
}
Step Six
This code in the TVC lifecycle method viewWillDisappear
updates the data in the parent TVC when it is an NSArray
of managed objects that is being returned, or in the case that a single managed object ID row is simply deselected and no other row is selected (in the case that the row was selected (check marked) on entering the table view / TVC).
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
NSLog(@"%@ - %@ - values for:\n indexPathSelected: %@\n indexPathObjectFromArray: %@\n\n", NSStringFromClass(self.class), NSStringFromSelector(_cmd), self.indexPathSelected, self.indexPathObjectFromArray);
if (self.passBackManagedObjects) {
if (self.isArray) {
// Return an array of selected NSManagedObjects
[self blockSelectManagedObjects](self.arrayObjectsSelected);
} else if (self.indexPathSelected == nil) {
// Return nil where a previously selected (optional) entity is deactivated
[self blockSelectManagedObjectID](nil);
}
}
}
Hope you enjoy working through this little treat! Any questions feel free to ask.
回答2:
You use the same data source so that technically you do not sync. Use a single set of objects to do the check.
Any UITableView
subclass needs to call the UITableViewDataSource
tableView:cellForRowAtIndexPath:
method when reloadData
. That's where you do something like
if (![selectionSet containsObject:object]) {
[tableView selectRowAtIndexPath:indexPath animated:NO];
[cell setSelected:YES animated:NO];
}
else {
[tableView deselectRowAtIndexPath:indexPath];
[cell setSelected:NO animated:NO];
}
It's best to override the UITableViewCell
setSelected:animated:
method too IMO so you can have more control over UITableViewDelegate
method tableView:didSelectRowAtIndexPath:
.
Good luck, hope this helps~
来源:https://stackoverflow.com/questions/25455135/uitableviewcontroller-with-uisearchdisplaycontroller-multiple-selection-sync