UITableViewController with UISearchDisplayController multiple selection sync

ε祈祈猫儿з 提交于 2019-12-25 06:19:13

问题


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 of UISearchController.
  • To attempt to keep the answer as short as possible, this solution leaves out chunks of code necessary to properly prepare both self.tableView and self.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 NSManagedObjects (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 UITableViewCells 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!