UITableView indexPath 和最大可重用单元格数

Posted

技术标签:

【中文标题】UITableView indexPath 和最大可重用单元格数【英文标题】:UITableView indexPath and maximum number of reusable cells 【发布时间】:2018-03-08 13:46:50 【问题描述】:

我的UITableView 使用自定义可重用单元已完成,但仍然存在两个问题:

    我可以创建 6 个单元格,但是当我添加第 7 个单元格时,应用程序崩溃并显示Unexpectedly found nil while unwrapping an Optional value,我真的无法理解。当我重新启动应用程序时,view 立即再次崩溃。

    除了valueLabel.text 之外,单元格的数据被正确地重复使用。 当我在滚动tableView 的单元格上更新此值时,数据会退回一行(如图所示)。我相信这是与data source 的错误编辑(更新?)有关的indexPath 问题,但是当我重新启动应用程序时,数据位于正确的单元格中。

我在下面的代码中标记了这些事件 1 和 2。

创建数据:

func createCryptoArray(_ addedCrypto: String) 

        if addedCrypto == "Bitcoin BTC" 
            if CoreDataHandler.saveObject(name: "Bitcoin", code: "bitcoin", symbol: "BTC", placeholder: "BTC Amount", amount: "0.00000000", amountValue: "0.0") 
                for _ in CDHandler.fetchObject()! 
                
            
        
        if addedCrypto == "Bitcoin Cash BCH" 
            if CoreDataHandler.saveObject(name: "Bitcoin Cash", code: "bitcoinCash", symbol: "BCH", placeholder: "BCH Amount", amount: "0.00000000", amountValue: "0.0") 
                for _ in CDHandler.fetchObject()! 
                
            
         
        //...
    

WalletTableViewController:(有问题1)

class WalletTableViewController: UIViewController, UITextFieldDelegate 

@IBOutlet weak var tableView: UITableView!

var cryptos : [CryptosMO] = []

var total : Double = 0.0
static var staticTotal : Double = 0.0

override func viewDidLoad() 
    super.viewDidLoad()

    tableView.delegate = self
    tableView.dataSource = self

    if CDHandler.fetchObject() != nil 
        cryptos = CDHandler.fetchObject()!
        tableView.reloadData()
    

    update()

    updateWalletValue()
    updateWalletLabel()


override func viewWillAppear(_ animated: Bool) 

    update()

    tableView.delegate = self
    tableView.dataSource = self

    if CDHandler.fetchObject() != nil 
        cryptos = CDHandler.fetchObject()!
        tableView.reloadData()
    


func updateCellValue() 

    for section in 0...self.tableView.numberOfSections - 1 
        if (self.tableView.numberOfRows(inSection: section) >= 1) 
            for row in 0...self.tableView.numberOfRows(inSection: section) - 1 
                let indexP: IndexPath = IndexPath(row: row, section: section);
                self.updateCellValueLabel(self.tableView.cellForRow(at: indexP) as! WalletTableViewCell) // <-------Problem 1
            
        
    


func update() 

    updateCellValue()


TableView 函数:

extension WalletTableViewController: UITableViewDelegate, UITableViewDataSource, CryptoCellDelegate 

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
    return cryptos.count


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 

    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! WalletTableViewCell

    cell.cryptoNameLabel.text = cryptos[indexPath.row].name
    cell.cryptoCodeLabel.text = cryptos[indexPath.row].symbol
    cell.amountLabel.text = cryptos[indexPath.row].amount
    cell.amountTextField.placeholder = cryptos[indexPath.row].placeholder

    if cryptos[indexPath.row].amountValue == "0.0" 
        cell.cryptoValueLabel.text = ""
    

    cell.delegate = self
    cell.amountTextField.delegate = self

    return cell


func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) 

    if editingStyle == .delete 
        let selectedManagedObject = cryptos[indexPath.row]
        CDHandler.deleteObject(entity:"CryptosMO", deleteObject: selectedManagedObject)
        cryptos.remove(at: indexPath.row)
        tableView.deleteRows(at: [indexPath], with: .fade)
        updateWalletValue()
        updateWalletLabel()
    

用户操作、计算和数据更新:(使用valueLabel.text's

// TextFields amounts
//--------------------
func cellAmountEntered(_ walletTableViewCell: WalletTableViewCell) 

    if walletTableViewCell.amountTextField.text == "" 
        return
    

    let str = walletTableViewCell.amountTextField.text
    let formatter = NumberFormatter()
    formatter.locale = Locale(identifier: "en_US")
    let dNumber = formatter.number(from: str!)
    let nDouble = dNumber!
    let eNumber = Double(truncating: nDouble)
    walletTableViewCell.amountLabel.text = String(format:"%.8f", eNumber)

    var editAmount = ""
    editAmount = String(format:"%.8f", eNumber)

    let indexPath = tableView.indexPath(for: walletTableViewCell)
    let selectedManagedObject = cryptos[(indexPath?.row)!]

    CDHandler.editObject(editObject: selectedManagedObject, amount: editAmount, amountValue: "0.0")

    walletTableViewCell.amountTextField.text = ""



// Value calculation & label update
//----------------------------------
func updateCellValueLabel(_ walletTableViewCell: WalletTableViewCell) 

    if walletTableViewCell.amountLabel.text == "" 
        walletTableViewCell.amountLabel.text = "0.00000000"
    

    var newCryptos : [CryptosMO] = []
    var doubleAmount = 0.0

    var cryptoPrice = ""
    let indexPath = tableView.indexPath(for: walletTableViewCell)

    if CDHandler.fetchObject() != nil 
        newCryptos = CDHandler.fetchObject()!
    

    cryptoPrice = cryptos[(indexPath?.row)!].code!
    guard let cryptoDoublePrice = CryptoInfo.cryptoPriceDic[cryptoPrice] else  return 

    let selectedAmount = newCryptos[(indexPath?.row)!]

    guard let amount = selectedAmount.amount else  return 
    var currentAmountValue = selectedAmount.amountValue

    doubleAmount = Double(amount)!

    let calculation = cryptoDoublePrice * doubleAmount
    currentAmountValue = String(calculation)

    CoreDataHandler.editObject(editObject: selectedAmount, amount: amount, amountValue: currentAmountValue)

    walletTableViewCell.valueLabel.text = String(calculation) // <-------- `valueLabel.text`

CoreData 函数:

class CoreDataHandler: NSObject 

class func getContext() -> NSManagedObjectContext 
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    return appDelegate.persistentContainer.viewContext


class func saveObject(name:String, code:String, symbol:String, placeholder:String, amount:String, amountValue:String) -> Bool 
    let context = getContext()
    let entity = NSEntityDescription.entity(forEntityName: "CryptosMO", in: context)
    let managedObject = NSManagedObject(entity: entity!, insertInto: context)

    managedObject.setValue(name, forKey: "name")
    managedObject.setValue(code, forKey: "code")
    managedObject.setValue(symbol, forKey: "symbol")
    managedObject.setValue(placeholder, forKey: "placeholder")
    managedObject.setValue(amount, forKey: "amount")
    managedObject.setValue(amountValue, forKey: "amountValue")

    do 
        try context.save()
        return true
     catch 
        return false
    


class func fetchObject() -> [CryptosMO]? 
    let context = getContext()
    var cryptos: [CryptosMO]? = nil

    do 
        cryptos = try context.fetch(CryptosMO.fetchRequest()) as? [CryptosMO]
        return cryptos
     catch 
        return cryptos
    


class func deleteObject(entity: String, deleteObject: NSManagedObject) 
    let context = getContext()
    context.delete(deleteObject)

    do 
        try context.save()
     catch let error as NSError 
        print("Could not save. \(error), \(error.userInfo)")
    


class func editObject(editObject: NSManagedObject, amount: String, amountValue: String) 
    let context = getContext()
    let managedObject = editObject

    do 
        managedObject.setValue(amount, forKey: "amount")
        managedObject.setValue(amountValue, forKey: "amountValue")
        try context.save()
     catch let error as NSError 
        print("Could not save. \(error), \(error.userInfo)")
    



【问题讨论】:

self.tableView.cellForRow(at:) 如果单元格不可见,则返回 nil。既然你在那里用“!”解包,这就解释了崩溃。 您应该删除updateCellValue()UITableView自动调用tableView(_, cellForRowAt:)当它需要时,你只需要更新你的信息数组并调用tableView.reloadData()。表格视图会在可见单元格需要时自动使用该方法。这是一种被动的方式,它是一个委托 @AlejandroIván 我在哪里以及如何更新我的信息数组? @martin 你的cryptos 数组。我指的是用于填充表格视图的数组。 @Larme in tableView.cellForRow(at:)updateCellValue() ? 【参考方案1】:

您使用 UIKit 框架的方式与预期完全相反:您的代码循环通过您的数据模型并希望更新表格视图中的每个单元格。想象你有数千行,那么你(至少)会尝试更新大量的单元格。但是,正如@Larme 已经提到的那样,这些单元格不存在(因为它们不可见),并且您的大多数调用都是徒劳的。

要正确使用 UIKit,你必须改变你的方法:你不会主动更新,而是如果 table view 检测到它必须在一个单元格(或多个单元格,一个接一个),因为视图变得可见,用户滚动等。所以如果表格视图只显示 5 个单元格,它只要求 5 个单元格的数据,而不是整个数据库。

你要做的是:

删除func updateCellValue()。如果您认为必须更新视图,请在表格视图中调用reloadData()reloadRowsAtIndexPaths...。这将导致调用适当的数据源方法,例如... 在tableView(cellForRowAt:) 的末尾,您使用出列单元格调用updateCellValueLabel,然后将更新标签。 您还应该修改updateCellValueLabel并交出IndexPath(或只交该行),这样您就不需要再拨打tableView.indexPath(for: walletTableViewCell)了。

这只是一个简单的提示;你也可以考虑优化数据获取


补充关于第三点(见评论): 您可以通过以下方式修改您的代码:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 

    // ... same as before

    cell.delegate = self
    cell.amountTextField.delegate = self

    updateCellValueLabel(cell, atRow:indexPath.row)
    return cell


func updateCellValueLabel(_ walletTableViewCell: WalletTableViewCell, atRow row:Int) 

    if walletTableViewCell.amountLabel.text == "" 
        walletTableViewCell.amountLabel.text = "0.00000000"
    

    var newCryptos : [CryptosMO] = []
    var doubleAmount = 0.0

    var cryptoPrice = ""

    if CDHandler.fetchObject() != nil 
        newCryptos = CDHandler.fetchObject()!
    

    cryptoPrice = cryptos[row].code!
    guard let cryptoDoublePrice = CryptoInfo.cryptoPriceDic[cryptoPrice] else  return 

    let selectedAmount = newCryptos[row]

    guard let amount = selectedAmount.amount else  return 
    var currentAmountValue = selectedAmount.amountValue

    doubleAmount = Double(amount)!

    let calculation = cryptoDoublePrice * doubleAmount
    currentAmountValue = String(calculation)

    CoreDataHandler.editObject(editObject: selectedAmount, amount: amount, amountValue: currentAmountValue)

    walletTableViewCell.valueLabel.text = String(calculation) // <-------- `valueLabel.text`

所以updateCellValueLabel 获得了一个额外的atRow 参数,以访问cryptosnewCryptos 数组中的正确值。

【讨论】:

非常感谢安德烈亚斯,这似乎是完美的答案!有几件事我不完全理解。这周我一直在同时学习tableViewsCoreData,对此感到抱歉。我正确地理解了你的第二点,但你能在其他两个点上增加一点精度吗?特别是hand in the IndexPath (or only the row), so you do not need to call tableView.indexPath。再次感谢! 它完美运行安德烈亚斯!还有两个问题:通过委托调用updateCellValueLabel 时,如何从另一个类传递indexPath.row?你会建议我阅读什么以更好地理解tableView 逻辑?再次感谢:) 要了解这些内容,您可以访问开发人员培训或查看斯坦福大学等在线培训 - 只需在 youtube 上搜索“cs193p”,还有一个有关表格视图的视频。我不太明白第一个问题,请更准确 - indexPath.row 是您的数据模型中显示在单元格中的索引。 我会检查这些!是的,我忘记了这个想法,只是从cellAmountEntered() 调用tableView.reloadData(),所以表本身会调用updateCellValueLabel()【参考方案2】:

真正的问题在于您更新单元格的逻辑。您使用一个名为updateCellValue() 的函数,该函数旨在“从外部”强制更新单元格。这是错误的。相反,您将更新您的数据源并让 tableView 通过调用 self.tableView.reloadData() 来拉取这些更改。永远不要通过更新单元格来更新表格视图,而是通过简单地更新数据源来更新表格视图。

【讨论】:

以上是关于UITableView indexPath 和最大可重用单元格数的主要内容,如果未能解决你的问题,请参考以下文章

如何在 UITableView 中获取选定的 indexPath.section 和 indexPath.row 值?

在 UITableView 中基于 indexPath 获取对象的替代方法

tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) 没有被调用

UITableView 的 indexPathForCell:总是返回 0 作为 indexPath.row

UITableView,cellForRowAt indexPath 没有被调用

func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) 几乎符合可选要求