Search code examples
iosswiftuicollectionviewuicollectionviewcellcollectionview

Swift slow custom keyboard buttons with collectionviews


Disclaimer:

Before you rip my head off and set it on fire, I know questions like this have already been asked but they all refer to single buttons. I have a collectionView. I'm asking this question after carefully reading all the other questions and trying to combine their solutions together.

Overview:

I'm creating a keyboard extension with a collectionview. In my collectionView I have custom cells and in their class I placed a button. I want to add a target to each button in the collection view. I know I could use the selectItemAtIndexRow function and ignore the buttoms but I need to handle the touchUpInside and touchDown events (because when I type with y keyboard it's super slow) and I haven't found a way to do it with collectionView cells (if something exists let me know). In order to escape this problem, I thought the best solution could have been adding a button to my cell's class and add the actions I wanted to it, but I found multiple prooblems doing so.

What I'm doing:

  1. I have the classic keyboardViewController you get by creating a new keyboard target where I placed a view (which is the view containing the collectionView).

  2. I have a custom view class which contains the collection view

  3. I have a custom collectionview cell class

Custom View Class

Here I programmatically created my collectionView.

//Closure to add action to my buttons
 var insertLowercase : ((IndexPath) -> ())?
    
 let letters = ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]

 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        letters.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = keyView.dequeueReusableCell(withReuseIdentifier: "collectionCellId", for: indexPath) as! KeyboardKeys
        cell.button.setTitle(letters[indexPath.row], for: .normal)
        cell.button.addTarget(self, action: #selector(lettersKeyboard.insert(indexPath:)), for: .touchUpInside)
        return cell
    }
    
    @objc func insert (indexPath: IndexPath){
        insertLowercase?(indexPath)
    }

This is how I add the target. However if I press the button, the keyboard crashes giving me the following error:

Thread 1: "-[Keyboarddd.KeyboardButton length]: unrecognized selector sent to instance 0x7f9bb2506740"

Keyboard ViewController

After putting here my custom view, in the viewDidLoad I call this:

 letters.insertLowercase = { indexPath in
            let text = self.letters.letters[indexPath.row]
            self.textDocumentProxy.insertText(text)
        } //And more stuff to handle quick insertion but this is what you need to reproduce my problem

Question:

How can I decently add the target to my button? Or is there a way to do what I want to do with the collectionView cells directly?


Solution

  • Your problem is that UIButton.addTarget doesn't call a method that takes an IndexPath as a parameter. Typically what you do is handle the button action in the cell, and then invoke a callback when the button is pressed.

    class KeyboardKeys: UICollectionViewCell {
    
        @IBOutlet weak var button: UIButton! {
            didSet {
                button.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
            }
        }
    
        var didPushButton: (KeyboardKeys) -> Void = { _ in }
    
        @objc func buttonAction(_ button: UIButton) {
            didPushButton(self)
        }
    }
    

    In your data source method you want to register a callback with each cell:

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCellId", for: indexPath) as! KeyboardKeys
        cell.button.setTitle(letters[indexPath.row], for: .normal)
        cell.didPushButton = { [weak self] cell in
            guard let indexPath = collectionView.indexPath(for: cell) else { return }
            self?.insert(indexPath: indexPath)
        }
        return cell
    }