Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

UICollectionView with orthogonal (horizontal) section not calling touchesShouldCancel(in:)

I have a UICollectionView with horizontally scrolling sections. In the cell I have a UIButton. I need to cancel the touches when the user swipes horizontally but it does not work.

touchesShouldCancel(in:) is only called when swiping vertically over the UIButton, not horizontally. Is there a way to make it work?

Sample code below

import UIKit

class ConferenceVideoSessionsViewController: UIViewController {

    let videosController = ConferenceVideoController()
    var collectionView: UICollectionView! = nil
    var dataSource: UICollectionViewDiffableDataSource
        <ConferenceVideoController.VideoCollection, ConferenceVideoController.Video>! = nil
    var currentSnapshot: NSDiffableDataSourceSnapshot
        <ConferenceVideoController.VideoCollection, ConferenceVideoController.Video>! = nil
    static let titleElementKind = "title-element-kind"

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Conference Videos"
        configureHierarchy()
        configureDataSource()
    }
}

extension ConferenceVideoSessionsViewController {
    func createLayout() -> UICollectionViewLayout {
        let sectionProvider = { (sectionIndex: Int,
            layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                 heightDimension: .fractionalHeight(1.0))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)

            // if we have the space, adapt and go 2-up + peeking 3rd item
            let groupFractionalWidth = CGFloat(layoutEnvironment.container.effectiveContentSize.width > 500 ?
                0.425 : 0.85)
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(groupFractionalWidth),
                                                  heightDimension: .absolute(200))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

            let section = NSCollectionLayoutSection(group: group)
            section.orthogonalScrollingBehavior = .continuous
            section.interGroupSpacing = 20
            section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)

            return section
        }

        let config = UICollectionViewCompositionalLayoutConfiguration()
        config.interSectionSpacing = 20

        let layout = UICollectionViewCompositionalLayout(
            sectionProvider: sectionProvider, configuration: config)
        return layout
    }
}

extension ConferenceVideoSessionsViewController {
    func configureHierarchy() {
        collectionView = MyUICollectionView(frame: .zero, collectionViewLayout: createLayout())
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.backgroundColor = .systemBackground
        view.addSubview(collectionView)
        NSLayoutConstraint.activate([
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            ])

        collectionView.canCancelContentTouches = true
    }

    func configureDataSource() {
        
        let cellRegistration = UICollectionView.CellRegistration
        <ConferenceVideoCell, ConferenceVideoController.Video> { (cell, indexPath, video) in
            // Populate the cell with our item description.
            cell.buttonView.setTitle("Push, hold and swipe", for: .normal)
            cell.titleLabel.text = video.title
        }
        
        dataSource = UICollectionViewDiffableDataSource
        <ConferenceVideoController.VideoCollection, ConferenceVideoController.Video>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, video: ConferenceVideoController.Video) -> UICollectionViewCell? in
            // Return the cell.
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: video)
        }
        
        currentSnapshot = NSDiffableDataSourceSnapshot
            <ConferenceVideoController.VideoCollection, ConferenceVideoController.Video>()
        videosController.collections.forEach {
            let collection = $0
            currentSnapshot.appendSections([collection])
            currentSnapshot.appendItems(collection.videos)
        }
        dataSource.apply(currentSnapshot, animatingDifferences: false)
    }
}

class MyUICollectionView: UICollectionView {

    override func touchesShouldCancel(in view: UIView) -> Bool {
        print("AH: touchesShouldCancel view \(view.description)")
        if view is MyUIButton {
            return true
        }

        return false
    }
}

final class MyUIButton: UIButton {
}

class ConferenceVideoCell: UICollectionViewCell {

    static let reuseIdentifier = "video-cell-reuse-identifier"
    let buttonView = MyUIButton()
    let titleLabel = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }
    required init?(coder: NSCoder) {
        fatalError()
    }
}

extension ConferenceVideoCell {
    func configure() {
        buttonView.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(buttonView)
        contentView.addSubview(titleLabel)

        titleLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
        titleLabel.adjustsFontForContentSizeCategory = true

        buttonView.layer.borderColor = UIColor.black.cgColor
        buttonView.layer.borderWidth = 1
        buttonView.layer.cornerRadius = 4
        buttonView.backgroundColor = UIColor.systemPink

        let spacing = CGFloat(10)
        NSLayoutConstraint.activate([
            buttonView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            buttonView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            buttonView.topAnchor.constraint(equalTo: contentView.topAnchor),

            titleLabel.topAnchor.constraint(equalTo: buttonView.bottomAnchor, constant: spacing),
            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
            ])
    }
}

Created FB18103483 with a sample project.

UICollectionView with orthogonal (horizontal) section not calling touchesShouldCancel(in:)
 
 
Q