Mac Catalyst: tableView allowmultipleselection not working
Asked Answered
N

6

14

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
}
Niggardly answered 25/3, 2020 at 20:8 Comment(0)
B
16

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.

Bellwort answered 30/3, 2020 at 5:34 Comment(5)
Thank you for the detailed and clear answer and your project clearly demonstrates the behavior. I also don't agree with this approach as - as you said - it is very hard to discover and the behavior should be consistent for users across their devices. I guess the only way forward is to submit feedback to apple and hope for the best! Thanks again.Niggardly
Looks like Apple has deliberately chosen this behavior to be consistent with macOS behavior, however, there is absolutely no documentation about it as of this writing. Our app doesn't look like a mac app, it looks like an iPad app (because of Catalyst). My opinion is that users will interact with the app as though it was an iPad app not a mac app; therefore, won't think of command + click to perform multiselection.Phosphorate
Hi :) In my app I don't need to enter edit-mode and so I only have tableView.allowsMultipleSelection = true. This works just fine in iOS to select one or more cells. But on Catalyst that doesn't work with Shift/Command+click... anyone having luck in selecting multiple cells in this mode at all? Thanks! :)Cornerstone
@JensSchwarzer yes it is working, you should re-read the answer. It is working keeping COMMAND ⌘ pressed, and select multiple with no tableView.allowsMultipleSelection = trueJauch
@SulmanMalik it has been a while; but it was indeed broken on the first version of Catalyst, but then started to work in the last versions... don't recall exactly when though :)Cornerstone
V
13

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
Vertebra answered 28/5, 2020 at 9:53 Comment(4)
Thanks for sharing this workaround; for me, every selected cell "blinks" every time a new one is selected / deselected; is this happening to you too? Have you found a workaround for this?Caesarism
I have also found a workaround for that and edited the answer accordingly. You have to overwrite shouldHighlightRowAtVertebra
That is an excellent workaround. When editing is ended, I found it necessary to also remove the highlights that were added to mitigate the flash/flickering. Also, when selections are made from code, say to implement a Select All function, the problem remains because .selectRow(at:animated:scrollPosition:) doesn't call the willSelectRowAt or willDeselectRowAt delegate methods. Although the overall multiple selection behavior is what was intended, the current behavior of .selectRow(at:animated:scrollPosition:) is apparently a bug. Hopefully it will be fixed in macOS 11.Bellwort
This workaround works. However the function tableView.deselectRow(at: indexPath) does not call the tableview delegate method deselectRowAtIndexPath. If you have implemented this delegate method, you might want to add the the following line self.tableView(tableView, didDeselectRowAt: indexPath) immediately after tableView.deselectRow(at: indexPath, animated: false).Schaller
E
2

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://mcmap.net/q/797872/-mac-catalyst-tableview-allowmultipleselection-not-working/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
}
Endocranium answered 10/8, 2020 at 10:30 Comment(0)
C
1

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!

Cornerstone answered 23/10, 2020 at 17:59 Comment(4)
I'm using Big Sur and the behavior outlined by the OP is still present in my Mac Catalyst App as of September 2021.Egret
@Egret did you use the Command key to select more? And also the Shift key for range? I don't have my app here so I can't try myself at the moment, sorry.Cornerstone
The command key and shift key work perfectly fine—but neither I nor most users would ever think to do that for a Mac Catalyst app like mine that uses the iPad idiom (i.e. not "optimized for mac"). The other answers here are in agreement that this behavior is either a bug or a misguided decision on Apple's part. In porting my iPad app to Mac Catalyst, I expect all the user interactions to remain the same, since they appear nearly identical. Ultimately, I implemented Rudolf Adamkovič's class-based solution above, which works like a charm in restoring the expected UX—click to select/deselect.Egret
OK I see. My comment was about it being completely broken, but then it started to work in Big Sur. I think whether if it matches iPad experience or not is another discussion :)Cornerstone
C
1

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
Characterize answered 24/4, 2021 at 12:58 Comment(0)
S
0

I had the same problem but with UICollectionView on Catalyst. I couldn't implement this same solution as UICollectionView is missing some of the delegate functions. I found this gist that solved it for me. https://gist.github.com/stefanceriu/5ff0c67e98ae44612857cd17fd4377d1

Sociolinguistics answered 2/3, 2022 at 23:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.