How to solve "unrecognized selector sent to instance" crash

My application offers IAP. On the list screen a Buy button is generated as an Accessory. Pressing Buy starts the IAP process. It goes well to the end (product is now purchased). Instead of generating a Checkmark in the place of the Buy after purchase, I generate a Download button, as an Accessory again. Pressing the Download button should save the product content and then a Checkmark should appear. At the moment, the Download button produces the crash with "unrecognized selector sent to instance".


In ProductTableViewCell swift which is for managing the View Cell, I have the following :

// Préparation de l'affichage de l'accessoire

var product: SKProduct? {

didSet {

guard let product = product else { return }

let defaults = UserDefaults.standard

let statusCurrentProduct = defaults.string(forKey: product.productIdentifier)

//print("Accesory pour \(product.productIdentifier)")

// Si produit déjà acheté, afficher une checkmark ou un Download

if MyBridgeStoreProducts.store.isProductPurchased(product.productIdentifier) {

if statusCurrentProduct == "Purchased" { // Acheté mais Pas encore sauvé

//print("générer un bouton Save")

accessoryType = .none

accessoryView = self.newDownloadButton()

}

else {

accessoryType = .checkmark // "Saved"

accessoryView = nil

}

// Si produit achetable, affichage Buy button

} else if IAPHelper.canMakePayments() {

//ProductTableViewCell.priceFormatter.locale = product.priceLocale WARNING: garder pour la syntaxe

//sérieLabel.text = ProductTableViewCell.priceFormatter.string(from: product.price) WARNING: garder pour la syntaxe

accessoryType = .none

accessoryView = self.newBuyButton()

} else { // Achat non autorisé

// detailTextLabel?.text = "Not available" // WARNING: code à conserver pour référence, mais je ne gère pas ce statut

}

}

}


Then the code for newDownloadButton is :

// Description du bouton Download

func newDownloadButton() -> UIButton {

let button = UIButton(type: .system)

button.setTitleColor(tintColor, for: .normal)

button.setTitle(NSLocalizedString("buttonDowload", tableName: "Magasin", value: "Download", comment: " "), for: .normal)

button.addTarget(self, action: #selector(ProductTableViewController.downloadButtonTapped(_:)), for: .touchUpInside)

button.sizeToFit()

return button

}


Then, in ProductTableViewController swift which manages the VC, I have the code for downloadButtonTapped as is :

// Action func pour bouton Download

@objc func downloadButtonTapped(_ sender: AnyObject) {

// Index de la ligne sélectionnée

let indexPath = tableView.indexPathForSelectedRow

// Recherche dans les User Defaults du produit sélectionné

let selectedProduct: String = headers[(indexPath?.section)!].série[(indexPath?.row)!].sérieAscId

let defaults = UserDefaults.standard

var statusCurrentProduct = defaults.string(forKey: selectedProduct)

if statusCurrentProduct == "Purchased" { // Pas encore sauvé WARNING: normalement, c'est toujours du Purchased (on pourrait supprimer le if)

// Sauvegarde de la série

let title: String = "Information"

let message: String = NSLocalizedString("msgInformationSave", tableName: "Magasin", value: "Saving the purchased series on this device", comment: " ")

showAlert(message: message, title: title)

saveSérie()

// flag le Produit acheté et sauvé

statusCurrentProduct = "Saved"

defaults.set(statusCurrentProduct, forKey: selectedProduct)

// Reload la table pour avoir la checkmark

tableView.reloadData()

}

}


My question is simple, what's wrong above to clear that crash. Thanks for your help.

Replies

C'est pas très clair…


We do not see in which class is each fragment of code


OK, this one is In ProductTableViewCell swift which is for managing the View Cell, I have the following :


    var product: SKProduct? {
        didSet {
            guard let product = product else { return }
            let defaults = UserDefaults.standard
            let statusCurrentProduct = defaults.string(forKey: product.productIdentifier)
        
            //print("Accesory pour \(product.productIdentifier)")
            // Si produit déjà acheté, afficher une checkmark ou un Download
            if MyBridgeStoreProducts.store.isProductPurchased(product.productIdentifier) {
                if statusCurrentProduct == "Purchased" {    // Acheté mais Pas encore sauvé
                    //print("générer un bouton Save")
                    accessoryType = .none
                    accessoryView = self.newDownloadButton()
                }
                else {
                    accessoryType = .checkmark  // "Saved"
                    accessoryView = nil
                }
            // Si produit achetable, affichage Buy button
            } else if IAPHelper.canMakePayments() {
                //ProductTableViewCell.priceFormatter.locale = product.priceLocale  WARNING: garder pour la syntaxe
                //sérieLabel.text = ProductTableViewCell.priceFormatter.string(from: product.price) WARNING: garder pour la syntaxe
                accessoryType = .none
                accessoryView = self.newBuyButton()
            } else {    // Achat non autorisé
                // detailTextLabel?.text = "Not available"  // WARNING: code à conserver pour référence, mais je ne gère pas ce statut
            }
        }
    }

Where is this one ?


    func newDownloadButton() -> UIButton {
     
        let button = UIButton(type: .system)
        button.setTitleColor(tintColor, for: .normal)
        button.setTitle(NSLocalizedString("buttonDowload", tableName: "Magasin", value: "Download", comment: " "), for: .normal)
        button.addTarget(self, action: #selector(ProductTableViewController.downloadButtonTapped(_:)), for: .touchUpInside)
        button.sizeToFit()
        return button
    }

And this one is in ProductTableViewController swift which manages the VC, I have the code for downloadButtonTapped as is :


// Action func pour bouton Download
    @objc func downloadButtonTapped(_ sender: AnyObject) {
        // Index de la ligne sélectionnée
        let indexPath = tableView.indexPathForSelectedRow
      
        // Recherche dans les User Defaults du produit sélectionné
        let selectedProduct: String = headers[(indexPath?.section)!].série[(indexPath?.row)!].sérieAscId
        let defaults = UserDefaults.standard
        var statusCurrentProduct = defaults.string(forKey: selectedProduct)
      
        if statusCurrentProduct == "Purchased" {   // Pas encore sauvé WARNING: normalement, c'est toujours du Purchased (on pourrait supprimer le if)
            // Sauvegarde de la série
            let title: String = "Information"
            let message: String = NSLocalizedString("msgInformationSave", tableName: "Magasin", value: "Saving the purchased series on this device", comment: " ")
            showAlert(message: message, title: title)
          
            saveSérie()
          
            // flag le Produit acheté et sauvé
            statusCurrentProduct = "Saved"
            defaults.set(statusCurrentProduct, forKey: selectedProduct)
          
            // Reload la table pour avoir la checkmark
            tableView.reloadData()
        }
    }

Where is the crash exactly ?


How do you transition between views (if there are different views).


downloadButtonTapped(_:) is not a class func. Why do you call it as ProductTableViewController.downloadButtonTapped(_:)

Merci...

My answers :

The func newDownloadButton is in ProductTableViewCell also.


Crash happens when pressing the Download button.


There is only one view (the List view). No transition.


The downloadButtonTapped func is called as ProductTableViewController.downloadButtonTapped just because I copied from a similar piece of code (the one generating the Buy button at the same place). I thought these buttons were similar. That's no more than that to be honest.


At the end of the day, what I am trying to achieve is just to replace the Buy button (for IAP) by a Download button (for saving the purchased product), then replace it by a Checkmark when product is purchased and saved. But I have no formal opinion on how to do it :-)

OK, just change


        button.addTarget(self, action: #selector(ProductTableViewController.downloadButtonTapped(_:)), for: .touchUpInside)

with


        button.addTarget(self, action: #selector(downloadButtonTapped(_:)), for: .touchUpInside)

Ca devrait marcher.


And read in detail the swift book about Type methods, to learn how to use

That doesn't work. Builder says : Use of unresolved identifier 'downloadButtonTapped'

When you write `addTarget(self, #selector(...))`, `self` needs to respond to the selector. Whether the selector is prefixed with some differenct class or not does not affect.With writing `button.addTarget(self, action: #selector(ProductTableViewController.downloadButtonTapped(_:)), for: .touchUpInside)`, `self` (which is an instance of `ProductTableViewCell`) needs to implement the method `downloadButtonTapped(_:))`, unfortunately, Swift cannot detect if it really implements the method.


So, if you do not want to implement `downloadButtonTapped(_:))` in your `ProductTableViewCell`, you may need to find another way to add target to the button.

That's probably why it works for the Builder but not in Run mode. I tried to move back to ProductTableViewCell the function downloadButtonTapped but it's impossible. There are too many other functions unknown in this VC. So as you said, I need to find another way to add target to the button. Looking at the addTarget () structure in XCode, I don't see other options. Do you have an example to offer ?

There is also a removeTarget function. Would that help to remove the 'Buy' target before setting the "Download' (as both buttons are on the same Accessory) ?

Do you have an example to offer ?


It's hard to say something sure without seeing more context, but if you implement such action methods only in `ProductTableViewController`, adding a property holding the view controller in each cell can be one option.

class ProductTableViewCell: UITableViewCell {

    weak var newButtonTarget: ProductTableViewController?
    
    func newDownloadButton() -> UIButton {
        let button = UIButton(type: .system)
        button.setTitleColor(tintColor, for: .normal)
        button.setTitle(NSLocalizedString("buttonDowload", tableName: "Magasin", value: "Download", comment: " "), for: .normal)
        button.addTarget(newButtonTarget, action: #selector(newButtonTarget!.downloadButtonTapped(_:)), for: .touchUpInside)
        button.sizeToFit()
        return button
    }
    
    //...
}
class ProductTableViewController: UIViewController, UITableViewDataSource {
    
    //...
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CellID, for: indexPath) as! ProductTableViewCell
        cell.newButtonTarget = self
        //Other setups...
        return cell
    }

    //...
}

I was starting to implement your solution in func tableView, and I wonder now if I am not going to create a conflict with the existing cell commands (lines: 23 and 24) ???


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        // Dequeue Cell     Version avec style Custom et Grouped
        let cell = tableView.dequeueReusableCell(withIdentifier: "productCell", for: indexPath) as! ProductTableViewCell
      
        // Configure the cell
        let header = headers[indexPath.section]
        
        // Affichage différent suivant critère de tri choisi
        var uneSérie = ProductTableViewCell.PetitHeader(itemGauche: header.série[indexPath.row].providerName, itemCentre: header.série[indexPath.row].sérieName, itemDroit: header.série[indexPath.row].sérieStartDate)
                
        // Récupération de l'index du row traité pour lecture products
        var indexCourant: Int = 0


        let product = products[indexCourant]
        
        
        // Affichage des données
        cell.updateCells(with: uneSérie)

        // Affichage du bouton Buy
        cell.product = product
        cell.buyButtonHandler = { product in MyBridgeStoreProducts.store.buyProduct(product)
        }
        
        cell.showsReorderControl = true
        return cell
    }

Please remember you have never shown such `buyButtonHandler` till now. I cannot say any more without mroe context.

I am sorry. I didn't mean to confuse or hide info. It's just that I didn't want to flood with too many code lines.

It was a reasonable desicion and I do not mean to offend you. Just saying the fact that we cannot discuss if you are going to create a conflict with the existing cell commands or not without seeing the whole code of the `cell`.