11
votes

I have a tableview that allows multiple selection. I have set both allowsMultipleSelection and allowsMultipleSelectionDuringEditing to true in viewDidLoad and this is working perfectly on both iOS and iPadOS. I have decided to try out the Catalyst today and the app looks good except that I cannot select multiple rows in this view. Any ideas? Here is the code below. Many thanks in advance.

//allow multiple selection

override func viewDidLoad() 
{
    super.viewDidLoad()

    self.tableView.allowsMultipleSelection = true
    self.tableView.allowsMultipleSelectionDuringEditing = true
.....
}

//limit selection to 7 rows

override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    if let selectedItems = tableView.indexPathsForSelectedRows {

        if selectedItems.count > 6 {
            return nil
        }
    }
    return indexPath
}

@IBAction func doneButtonTapped(_ sender: UIBarButtonItem) {

...

    let selectedIndexPaths = tableView.indexPathsForSelectedRows
    if !selectedIndexPaths!.isEmpty {
        for index in selectedIndexPaths! {
            let selectedProcedure = fetchedResultsController?.object(at: index) as! Item
...

Rest of code to perform the required task
}
5

5 Answers

15
votes

Multiple selection on macOS Catalyst does not work in quite the same way as on iOS and iPadOS and this appears to be either a bug or an unfortunate choice of intended behavior.

On macOS Catalyst, if you have enabled multiple selection in edit mode by setting tableView.allowsMultipleSelectionDuringEditing to true, only one row at a time can be directly selected by clicking with the pointer. However, multiple selection of contiguous rows is enabled by selecting a first row and then holding down SHIFT while selecting a second row, and multiple selection of non-contiguous rows is enabled by selecting a first row and then holding down COMMAND while selecting additional rows. This is Mac-like behavior in that it is how multiple selection generally works on macOS. So it is possible that this was intended behavior. But if that is the case, it is behavior that is hard to discover, not what an iOS/iPadOS user might expect, and works differently than on iOS and iPadOS. And it causes other problems - for example, in code I have a "Select All" function that is able to select all rows from code on iOS/iPadOS, and this code doesn't work on macOS Catalyst.

I filed Feedback on this. There is a simple project on GitHub at WB2ISS/MultipleSelection that demonstrates the problem.

10
votes

While everything that was said here is true, there is an 'easy' way to hack this behaviour. With the code below you will get the same behaviour on Mac as on iOS/iPadOS

#if targetEnvironment(macCatalyst)
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    if let selectedRows = tableView.indexPathsForSelectedRows, selectedRows.contains(indexPath) {
        tableView.deselectRow(at: indexPath, animated: false)
        return nil
    }
    return indexPath
}

func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
    if let selectedRows = tableView.indexPathsForSelectedRows, selectedRows.contains(indexPath) {
        return nil
    }
    return indexPath
}

func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
    // the mac sets isHighlighted of each other cell to false before selecting them again which leads to a flickering of the selection. Therefore go through the selected cells and highlight them here manually
    tableView.indexPathsForSelectedRows?.forEach { tableView.cellForRow(at: $0)?.isHighlighted = true }
    return true
}
#endif
2
votes

Below is the solution provided by @ph1lb4 packaged as a standalone class. Importantly, this version calls didSelectRowAt when selecting rows which means that the subclasses relying on didSelectRowAt will not break.

import UIKit

// WORKAROUND:
// As of macOS 10.15 Catalina, multi-row selection in Catalyst apps is not
// intuitive. The user is expected to use the shift key to select multiple
// rows. See https://stackoverflow.com/q/60856636/1306956 for more details.
open class CatalystWorkaroundTableViewController: UITableViewController {
    #if targetEnvironment(macCatalyst)
    override open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        if let selectedRows = tableView.indexPathsForSelectedRows, selectedRows.contains(indexPath) {
            tableView.deselectRow(at: indexPath, animated: false)
            self.tableView(tableView, didSelectRowAt: indexPath)
            return nil
        } else {
            return indexPath
        }
    }

    override open func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
        if let selectedRows = tableView.indexPathsForSelectedRows, selectedRows.contains(indexPath) {
            return nil
        } else {
            return indexPath
        }
    }

    // WORKAROUND:
    // Catalyst de-highlights cells beofre selecting them again which results in flickering.
    override open func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
        for indexPath in tableView.indexPathsForSelectedRows ?? [] {
            tableView.cellForRow(at: indexPath)?.isHighlighted = true
        }
        return true
    }
    #endif
}
0
votes

Great news! In macOS Big Sur UITableView tableView.allowsMultipleSelection = true works just like in iOS! Happy days! You can also a select multiple cells programmatically!

0
votes

Bellow is the solution wrriten in objective-c.

Thank you. @ph1lb4

#if TARGET_OS_MACCATALYST
    
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    NSArray<NSIndexPath *> * selectedRows = [tableView indexPathsForSelectedRows];
    if ([selectedRows containsObject:indexPath]) {
        [tableView deselectRowAtIndexPath:indexPath animated:false];
        
        return nil;
    }

    return indexPath;
}

- (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSArray<NSIndexPath *> * selectedRows = [tableView indexPathsForSelectedRows];
    if ([selectedRows containsObject:indexPath]) {
        return nil;
    }
    return indexPath;
}

- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
    NSArray<NSIndexPath *> * selectedRows = [tableView indexPathsForSelectedRows];
    for(NSIndexPath *index in selectedRows){
        [[tableView cellForRowAtIndexPath:index] setHighlighted:YES];
    }
    
    return YES;
}

#else

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    return indexPath;
    
}

- (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
    return indexPath;
}


#endif