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.

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`.

How to solve "unrecognized selector sent to instance" crash
 
 
Q