skip to Main Content

I have a tableView with cells that contain UiCollectionView. My didSelect tableView’s delegate isn’t called when I touch the cell on the collectionView.
I think it’s my collectionView that get the touch instead. Do you have any elegant solution to keep the scroll enabled on my collectionView but disable the selection and pass it to the tableview ?

Here is my tableView declaration :

private lazy var tableView:UITableView = { [weak self] in
    
    $0.register(TestTableViewCell.self, forCellReuseIdentifier: "identifier")
    $0.delegate = self
    $0.dataSource = self
    return $0
    
}(UITableView())

Here are my delegate and dataSource methods:

public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
    return 20
}

public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! TestTableViewCell
    return cell
}

public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
    tableView.deselectRow(at: indexPath, animated: true)
    
    print(indexPath)
}

And here is my cell :

public class TestTableViewCell : UITableViewCell {

    private lazy var collectionViewFlowLayout:UICollectionViewFlowLayout = {
    
        $0.scrollDirection = .horizontal
        $0.minimumLineSpacing = 0
        $0.minimumInteritemSpacing = 0
        return $0
    
    }(UICollectionViewFlowLayout())
    private lazy var collectionView:UICollectionView = {
    
        $0.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "identifier")
        $0.delegate = self
        $0.dataSource = self
        return $0
    
    }(UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout))

    public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    
        contentView.addSubview(collectionView)
        collectionView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

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

    public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    
        return 5
    }

    public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
        let cell:UICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: cell:"identifier", for: indexPath) as! cell:UICollectionViewCell
        return cell
    }

    public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    
        return collectionView.frame.size
    }
}

If you spot any compilation error, excuse me, this is an anonymized copy/past. My app is running without error.

If you want an example of what I’m trying to do you can check AirBnb’s app. TableView with some houses with cells and inside, pictures collectionView. Il you touch the collectionView, the tableView select the cell…

Thanks

3

Answers


  1. Maybe you would like to try allowsSelection property of UICollectionView. If you set this property false, your collection view cells are not selected any more.

    And I think tableView didSelect can capture user interaction. If UITableView still can not capture didSelect, you can give delegate to collectionView and fire it once it’s tapped by adding a tap gesture onto UICollectionView.

    Login or Signup to reply.
  2. You can do the following

    1. First in first, print something in didSelectItem of your collectionView delegate, to make ensure that your collectionView cell is tapped.

    2. add a delegate property in your UICollectionViewClass and call the delegate in the DidSelectItem if the step 1 is performing correctly.

    3. In your UITableViewController, you have a function cellForRowAtIndexPath, here add the delegate property for the associate cell.

    4. If you can print something in your delegate function, then you are at your last step. Call super.didSelect..with your indexPath, because now you have everything to call didSelect manually.

    Login or Signup to reply.
  3. I don’t believe there is a "direct" way to do what you’re asking. The collection view will respond to the gestures, so they can’t "flow through" to the table view / cells without using a closure or protocol/delegate pattern.

    Here’s one approach…

    We subclass UICollectionView and, on touchesBegan call a closure to tell the table view to select the row.

    We can also implement scrollViewWillBeginDragging to select the row on collection view scroll, in addition to collection view "tap."

    So…

    View Controller class

    class TableColTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
        // number of items in the collection view for each row
        let myData: [Int] = [
            12, 15, 8, 21, 17, 14,
            16, 10, 5, 13, 20, 19,
        ]
    
        let tableView = UITableView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            tableView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(tableView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
            
            tableView.register(SomeTableCell.self, forCellReuseIdentifier: "someTableCell")
            tableView.dataSource = self
            tableView.delegate = self
        }
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return myData.count
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "someTableCell", for: indexPath) as! SomeTableCell
            cell.rowTitleLabel.text = "Row (indexPath.row)"
            cell.numItems = myData[indexPath.row]
            
            // closure for the cell to tell us to select it
            //  because its collection view was tapped
            cell.passThroughSelect = { [weak self] theCell in
                guard let self = self,
                      let pth = self.tableView.indexPath(for: theCell)
                else { return }
                
                self.tableView.selectRow(at: pth, animated: true, scrollPosition: .none)
                
                // selecting a row programmatically does NOT trigger didSelectRowAt
                //  so we'll call our own did select func
                self.myDidSelect(pth)
            }
            return cell
        }
        
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            // pass this on to our own did select func
            //  so it matches what happens when programmatically selecting the row
            myDidSelect(indexPath)
        }
        
        func myDidSelect(_ indexPath: IndexPath) {
            print("Table View - didSelectRowAt", indexPath)
            // do something because the row was selected
        }
    }
    

    Table View Cell class

    class SomeTableCell: UITableViewCell {
        
        // callback closure
        var passThroughSelect: ((UITableViewCell) -> ())?
        
        var rowTitleLabel = UILabel()
        var collectionView: SubCollectionView!
        var numItems: Int = 0 {
            didSet {
                collectionView.reloadData()
            }
        }
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            let fl = UICollectionViewFlowLayout()
            fl.scrollDirection = .horizontal
            fl.estimatedItemSize = CGSize(width: 120.0, height: 52.0)
            fl.minimumLineSpacing = 12
            fl.minimumInteritemSpacing = 12
            collectionView = SubCollectionView(frame: .zero, collectionViewLayout: fl)
            
            rowTitleLabel.translatesAutoresizingMaskIntoConstraints = false
            collectionView.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(rowTitleLabel)
            contentView.addSubview(collectionView)
            
            let g = contentView.layoutMarginsGuide
            
            // avoid auto-layout complaints
            let c = collectionView.heightAnchor.constraint(equalToConstant: 60.0)
            c.priority = .required - 1
            
            NSLayoutConstraint.activate([
                
                rowTitleLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                rowTitleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                rowTitleLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                rowTitleLabel.heightAnchor.constraint(equalToConstant: 30.0),
                
                collectionView.topAnchor.constraint(equalTo: rowTitleLabel.bottomAnchor, constant: 4.0),
                collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
                c,
                
            ])
            
            collectionView.register(SomeCollectionCell.self, forCellWithReuseIdentifier: "someCollectionCell")
            collectionView.dataSource = self
            collectionView.delegate = self
            
            collectionView.passThroughTouch = { [weak self] in
                guard let self = self else { return }
                self.passThroughSelect?(self)
            }
            
            collectionView.backgroundColor = .systemYellow
        }
        
    }
    extension SomeTableCell: UICollectionViewDataSource, UICollectionViewDelegate {
        func numberOfSections(in collectionView: UICollectionView) -> Int {
            return 1
        }
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return numItems
        }
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "someCollectionCell", for: indexPath) as! SomeCollectionCell
            cell.label.text = "Cell (indexPath.item)"
            return cell
        }
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            print("Collection View - didSelectItemAt", indexPath)
        }
    }
    extension SomeTableCell: UIScrollViewDelegate {
        // if you only want the table cell selected on TAP, don't implement this
        // if you want the table cell selected on CollectionView SCROLL, implement this
        func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
            self.passThroughSelect?(self)
        }
    }
    

    UICollectionView subclass

    class SubCollectionView: UICollectionView {
        var passThroughTouch: (() -> ())?
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            // this allows the collection view cell to be selected
            super.touchesBegan(touches, with: event)
            // this tells the controller (the table view cell)
            //  that the collection view was tapped
            passThroughTouch?()
        }
    }
    

    UICollectionView Cell class

    class SomeCollectionCell: UICollectionViewCell {
        
        var label = UILabel()
        var styleView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            styleView.translatesAutoresizingMaskIntoConstraints = false
            label.translatesAutoresizingMaskIntoConstraints = false
    
            styleView.addSubview(label)
            contentView.addSubview(styleView)
    
            let g = contentView
            NSLayoutConstraint.activate([
    
                styleView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                styleView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                styleView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                styleView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
    
                label.topAnchor.constraint(equalTo: styleView.topAnchor, constant: 8.0),
                label.leadingAnchor.constraint(equalTo: styleView.leadingAnchor, constant: 8.0),
                label.trailingAnchor.constraint(equalTo: styleView.trailingAnchor, constant: -8.0),
                label.bottomAnchor.constraint(equalTo: styleView.bottomAnchor, constant: -8.0),
                
            ])
    
            label.textColor = .white
            
            styleView.backgroundColor = UIColor(white: 0.5, alpha: 1.0)
            styleView.layer.borderColor = UIColor.white.cgColor
            styleView.layer.borderWidth = 1
            styleView.layer.cornerRadius = 6
    
        }
    
        override var isSelected: Bool {
            didSet {
                label.textColor = isSelected ? .red : .white
                styleView.backgroundColor = isSelected ? UIColor(white: 0.95, alpha: 1.0) : UIColor(white: 0.5, alpha: 1.0)
                styleView.layer.borderColor = isSelected ? UIColor.red.cgColor : UIColor.white.cgColor
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search