swifttvos

TVCardView in UICollectionViewCell doesn't support contextMenu


When using a TVCardView in a UICollectionViewCell the UICollectionViewDelegate contextMenuConfigurationForItemsAt function is not called.

Any idea if it's possible to work around this? I don't want to lose the default focus size increase effect of the standard card view, but I also would like to add support for context menus (now that they're finally available in tvOS 17)

Full reproducible example:

import UIKit
import TVUIKit

class ViewController: UICollectionViewController {
    private var dataSource: UICollectionViewDiffableDataSource<String, String>?

    override func viewDidLoad() {
        super.viewDidLoad()

        let tvCardCellRegistration = UICollectionView.CellRegistration<TVCardCell, String> { cell, _, _ in
        }

        let regularCellRegistration = UICollectionView.CellRegistration<RegularCell, String> { cell, _, _ in
            cell.contentView.backgroundColor = .orange
        }

        let dataSource = UICollectionViewDiffableDataSource<String, String>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            if indexPath.section == 0 {
                collectionView.dequeueConfiguredReusableCell(
                    using: tvCardCellRegistration,
                    for: indexPath,
                    item: itemIdentifier
                )
            } else {
                collectionView.dequeueConfiguredReusableCell(
                    using: regularCellRegistration,
                    for: indexPath,
                    item: itemIdentifier
                )
            }
        }

        self.dataSource = dataSource

        let collectionViewLayoutConfiguration = UICollectionViewCompositionalLayoutConfiguration()
        collectionViewLayoutConfiguration.interSectionSpacing = 40

        collectionView.delegate = self
        collectionView.setCollectionViewLayout(UICollectionViewCompositionalLayout(
            sectionProvider: { _, _ in
                let layoutSize = NSCollectionLayoutSize(
                    widthDimension: .absolute(220),
                    heightDimension: .absolute(220)
                )

                let group = NSCollectionLayoutGroup.horizontal(
                    layoutSize: layoutSize,
                    subitems: [
                        NSCollectionLayoutItem(layoutSize: layoutSize)
                    ]
                )

                let layout = NSCollectionLayoutSection(group: group)
                layout.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
                layout.contentInsetsReference = .none
                layout.interGroupSpacing = 20
                layout.contentInsets = .init(top: 12, leading: 60, bottom: 0, trailing: 60)
                return layout
            },
            configuration: collectionViewLayoutConfiguration
        ), animated: false)

        var snapshot = NSDiffableDataSourceSnapshot<String, String>()
        snapshot.appendSections(["section1", "section2"])
        snapshot.appendItems(["1", "2", "3", "4", "5"], toSection: "section1")
        snapshot.appendItems(["a", "b", "c", "d", "e"], toSection: "section2")

        dataSource.apply(snapshot, animatingDifferences: false)
    }

    @available(tvOS 17.0, *)
    override func collectionView(
        _ collectionView: UICollectionView,
        contextMenuConfigurationForItemsAt indexPaths: [IndexPath],
        point: CGPoint
    ) -> UIContextMenuConfiguration? {
        UIContextMenuConfiguration(actionProvider: { _ in
            let deleteAction = UIAction(
                title: "Delete",
                image: UIImage(systemName: "trash"),
                attributes: .destructive
            ) { _ in
                //
            }
            return UIMenu(title: "", children: [deleteAction])
        })
    }
}

class TVCardCell: UICollectionViewCell {
    let cardView = TVCardView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        cardView.cardBackgroundColor = .magenta
        cardView.contentSize = CGSize(
            width: 220,
            height: 220
        )
        contentView.addSubview(cardView)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        cardView.frame = contentView.bounds
    }
}

class RegularCell: UICollectionViewCell {}

Solution

  • So turns out if you add the TVCardView directly to the UICollectionViewCell rather than the contentView as you would normally do (and should, according to Apple), the context menu works fine.

    e.g.

    class TVCardCell: UICollectionViewCell {
        let cardView = TVCardView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            cardView.cardBackgroundColor = .magenta
            cardView.contentSize = CGSize(
                width: 220,
                height: 220
            )
            addSubview(cardView) // don't add to contentView here
        }
    
       ...