Is it wrong to add action to button in tableViewCell with tag?

Solution 1:

Will it work? yes, but as mentioned above, this is not the best approach, I'd avoid using tags unless this is just for some POC. There are better approaches to handle it. The first I'd suggest is using delegation to inform back to the controller, here's an example:

class BillHistoryTableViewController {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "BillHistoryTableViewCell", for: indexPath) as! BillHistoryTableViewCell
        let cellData = billHistories[indexPath.row]
        cell.setup(with: cellData)
        cell.index = indexPath.row
        
        cell.delegate = self
        return cell
    }
}

extension BillHistoryTableViewController: BillHistoryTableViewCellDelegate {
    func didTapButton(index: Int) {
        print("tapped cell with index:\(index)")

        if let id = billHistories[index].transactionInfo?.billUniqueID {
            hidePayIdGeneralTextField()
            billIdTextField.text = id.toNormalNumber()
            inquiryGeneralBillRequest()
        }
    }
}

protocol BillHistoryTableViewCellDelegate: AnyObject {
    func didTapButton(index: Int)
}

class BillHistoryTableViewCell: UITableViewCell {
    weak var delegate: BillHistoryTableViewCellDelegate?
    var cellData: CellData?
    var index: Int?
    
    func setup(with cellData: CellData) {
        self.cellData = cellData
    }
    
    @IBAction func buttonPressed(_ sender: UIButton) {
        guard let index = index else {
            return
        }
        
        delegate?.didTapButton(index: index)
    }
}

Another approach that I prefer lately is using Combine's PassThroughSubject, it requires less wiring and delegate definitions.

import Combine

class BillHistoryTableViewController {
    var cancellable: AnyCancellable?
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "BillHistoryTableViewCell", for: indexPath) as! BillHistoryTableViewCell
        let cellData = billHistories[indexPath.row]
        cell.setup(with: cellData)
        cell.index = indexPath.row
        
        cancellable = cell.tappedButtonSubject.sink { [weak self] index in
            guard let self = self else { return }
            print("tapped cell with index:\(index)")

            if let id = self.billHistories[index].transactionInfo?.billUniqueID {
                self.hidePayIdGeneralTextField()
                self.billIdTextField.text = id.toNormalNumber()
                self.inquiryGeneralBillRequest()
            }
        }
        
        return cell
    }
}

class BillHistoryTableViewCell: UITableViewCell {
    var tappedButtonSubject = PassthroughSubject<Int, Never>()
    
    var cellData: CellData?
    var index: Int?
    
    func setup(with cellData: CellData) {
        self.cellData = cellData
    }
    
    @IBAction func buttonPressed(_ sender: UIButton) {
        guard let index = index else {
            return
        }
        
        tappedButtonSubject.send(index)
    }
}

You can make it even shorter by injecting the index with the cellData, e.g:

func setup(with cellData: CellData, index: Int) {
    self.cellData = cellData
    self.index = index
}

but from what I see in your example, you don't even need the index, you just need the CellData, so if we'll take the Combine examples these are the main small changes you'll have to make:

var tappedButtonSubject = PassthroughSubject<CellData, Never>()

tappedButtonSubject.send(cellData)

and observing it by:

cancellable = cell.tappedButtonSubject.sink { [weak self] cellData in
    if let id = cellData.transactionInfo?.billUniqueID {
        //
    }
}