Search code examples
swiftuicollectionviewuibuttonuicollectionviewcelltvos

Swift: tvOS IBAction for UIControl in Collection View cell never gets called


I know this has been asked many times, but I am still learning, I have tried all possible solutions here in S.O. and I haven't had luck a single time. So this is the issue:

  • I am using Xcode 11 beta 3 (hopefully that ISN'T the issue!)
  • I have a simple collection view in a view controller
  • Inside the cell I have put a tvOS' TVPosterView - which inherits from UIControl and testing it by itself it does behave like a button (IBAction gets called).
  • On the collection view there is the animation when I push the poster
  • the poster view has a default image and I just change the title
  • I dragged an IBAction from the poster view to the cell class in IB
  • IBAction apart, the collection view shows and scrolls just fine, changing the titles of the posters
  • If I delegate the collection view to the view controller, collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) does get called.

Here is the code:

View Controller

import UIKit
import TVUIKit

class SecondViewController: UIViewController {

    @IBOutlet weak var myView: UIView!
    @IBOutlet weak var myCollection: MyCollection!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
}

extension SecondViewController: UICollectionViewDataSource {

    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

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

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell

        cell.myPoster.title! = "Header " + String(indexPath.row + 1)

        cell.myPoster.tag = indexPath.row + 1
        cell.posterTapAction = { cell in
            print("Header is: \(cell.myPoster.title!)")
        }

        return cell
    }
}

extension SecondViewController: UICollectionViewDelegate {

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// This one works - but that's not what I am looking for
        print("From didSelectItemAt indexPath: \(indexPath.item + 1)")
    }
}

Collection View

import UIKit

class MyCollection: UICollectionView {

    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
    }
}

Cell

import UIKit
import TVUIKit

class MyCell: UICollectionViewCell {

    var posterTapAction: ((MyCell) -> Void)?

    @IBOutlet weak var myPoster: TVPosterView!
    @IBAction func myAction(_ sender: TVPosterView) {
        posterTapAction?(self)
        print("Poster pressed \(sender.tag)")
    }
}

Any idea about what I am missing? I would just be happy to manage to print a dummy string after pressing a poster.

I have also tried the solutions with selectors and delegates, none worked. So, please let's concentrate on this particular solution with closures. Thanks!


Solution

  • I have finally found it. As usual it's due to tvOS focus. Basically the cell has to be not focusable, so that the UIControl element inside the cell (TVPosterView in my case, but it could be any UIControl) will be the focused one. It's tricking because graphically it does appear as if the poster had focus (one can rotate the poster a bit around).

    The solution is to add collectionView(_:canFocusItemAt:) to the UICollectionViewDelegate

    extension SecondViewController: UICollectionViewDelegate {
    
        func collectionView(_ collectionView: UICollectionView,
                                 canFocusItemAt indexPath: IndexPath) -> Bool {
            return false
        }
    
        func collectionView(_ collectionView: UICollectionView, 
                                 didSelectItemAt indexPath: IndexPath) {
            // Now, thanks to the function above, this is disabled
            print("From didSelectItemAt indexPath: \(indexPath.item + 1)")
        }
    }
    

    I have tested it in the code of this question and added it also to my project, which finally works!