UITableViewController 的单元格内容消失

Posted

技术标签:

【中文标题】UITableViewController 的单元格内容消失【英文标题】:UITableViewController's Cell's Content disappearing 【发布时间】:2017-07-19 20:02:20 【问题描述】:

我正在制作一个硬币收集应用程序,该应用程序旨在帮助用户维护其收藏的便携式记录。到目前为止,有两个视图控制器:CoinTableViewController 提供所有硬币类别的表格视图,CoinCategoryViewController 应该具有该类别中特定硬币的表格视图。

我显然希望我的代币能够根据多个标准(例如同一年、同一国家等)进行报告。为此,我创建了一个名为 CoinTableViewCell 的可重用 tableview 单元子类,它有 5 个 UILabel,它们代表收藏中的硬币可以按类别分组的所有信息。

这是我的故事板的样子

逻辑是根据我当前的排序标准,我可以隐藏单元格中的某些标签以反映硬币排序的标准。设置轮会打开一个菜单,该菜单出现在屏幕左侧,其中包含有关如何对硬币进行分类的选项,如果更改分类选项,则关闭菜单会使硬币类别控制器重新使用收藏中的硬币。

我的问题是,虽然我的程序总体上可以运行,但有时,某些单元格在程序使用它们后(在用户打开菜单并选择一个选项后)并没有完全显示。

这是这样的:

如您所见,视图控制器中的底部两个单元格缺少顶部标签,即使其他单元格有它们。由于我已经将 tableview 控制器的单元格实现为可调整大小,因此 table view 单元格应该自动调整大小以适应里面的内容。

这是我的代码:

//  Controls the table view controller showing the general coins (one per each category)
import UIKit
import CoreData

class CoinTableViewController: UITableViewController, NSFetchedResultsControllerDelegate, UISearchResultsUpdating, UITabBarControllerDelegate

    //this is an array of all the coins in the collection
    //each row of this two-dimensional array represents a new category
var coinsByCategory: [CoinCategoryMO] = []
var fetchResultController: NSFetchedResultsController<CoinCategoryMO>!

//we sort the coins by the category and then display them in the view controller
//example includes [ [Iraq Dinar 1943, Iraq Dinar 1200], etc. etc.]

//<OTHER VARIABLES HERE>

//the data here is used for resorting the coins into their respective categories

//the default sorting criteria is sorting the coins into categories with the same country, value, and currency
//and the user can change the app's sorting criteria by opening the ConfiguringPopoverViewController and changing the sorting criteria there
private var isCurrentlyResortingCoinsIntoNewCategories : Bool = false

override func viewDidLoad()

    super.viewDidLoad()
    
    self.tabBarController?.delegate = self
    
    //we now fetch the data
    let fetchRequest : NSFetchRequest<CoinCategoryMO> = CoinCategoryMO.fetchRequest()
    
    if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
    
        let context = appDelegate.persistentContainer.viewContext
        
        let sortDescriptor = NSSortDescriptor(key: "index", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]
        
        fetchResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
        fetchResultController.delegate = self
        
        do
        
            try fetchResultController.performFetch()
            if let fetchedObjects = fetchResultController.fetchedObjects
            
                self.coinsByCategory = fetchedObjects
            
        
        catch
        
            print(error)
        
    
            
    //if there is an empty area in the table view, instead of showing
    //empty cells, we show a blank area
    self.tableView.tableFooterView = UIView()
    
    //we configure the row heights for the table view so that the cells are resizable.
    //ALSO: should the user want to adjust the text size in "General"->"Accessibility"
    //the text size in the app will be automatically adjusted for him...
    tableView.estimatedRowHeight = 120
    tableView.rowHeight = UITableViewAutomaticDimension
    
    //WE CONFIGURE THE SEARCH BAR AND NAVIGATION BAR....

    //if the user scrolls up, he sees a white background, not a grey one
    tableView.backgroundView = UIView()


override func numberOfSections(in tableView: UITableView) -> Int 
    return fetchResultController.sections!.count


override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int

    if searchController != nil && searchController.isActive
    
        return searchResults.count
    
    else
    
        if let sections = fetchResultController?.sections
        
            return sections[section].numberOfObjects
        
        else
        
            return 0
        

    


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

    //configure the cell
    let cellIdentifier = "Cell"
    let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! CoinTableViewCell
    
    //Initialize the Cell
    let category = (searchController != nil && searchController.isActive) ? searchResults[indexPath.row] : coinsByCategory[indexPath.row]
    
    //we now remove the extra labels that we do not need
    cell.configureLabelsForCategoryType(theType: (category.coinCategory?.currentCategoryType)!)
    
    let sampleCoin : Coin = category.coinCategory!.getCoin(at: 0)!
    
    cell.countryLabel.text = "Country: \(sampleCoin.getCountry())"
    cell.valueAndDenominationLabel.text = "Value & Denom.: \(sampleCoin.valueAndDenomination)"
    
    //now we add in the quantity
    cell.quantityLabel.text = "Quantity: \(String(describing: coinsByCategory[indexPath.row].coinCategory!.count))"
    
    //we now add in the denomination
    cell.denominationOnlyLabel.text = "Denom.: \(sampleCoin.getDenomination())"
    
    //we now add in the year
    if sampleCoin.getYear() == nil
    
        cell.yearLabel.text = "Year: " + (Coin.DEFAULT_YEAR as String)
    
    else
    
        let yearABS = abs(Int32(sampleCoin.getYear()!))
        cell.yearLabel.text = "Year: \(yearABS) \(sampleCoin.getYear()!.intValue > 0 ? TimePeriods.CE.rawValue : TimePeriods.BCE.rawValue)"
    
    
    //we add in an accessory to indicate that clicking this cell will result in more information
    cell.accessoryType = .disclosureIndicator
    
    return cell


func deleteCoinCategory(rowPath: IndexPath)

    if 0 <= rowPath.row && rowPath.row < self.coinsByCategory.count
    
        //we have just tested that the rowPath index is valid
        if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
        
            let context = appDelegate.persistentContainer.viewContext
            let coinCategoryToDelete = self.fetchResultController.object(at: rowPath)
            context.delete(coinCategoryToDelete)
            
            appDelegate.saveContext()
            
            //ok we now deleted the category, now we update the indices
            updateIndices()
            appDelegate.saveContext()
        
    


func deleteCoin(c: Coin, indexOfSelectedCategory: IndexPath) -> Bool

    //we have a coin that we want to delete from this viewcontroller
    //and the data contained in it.
    //
    //the parameter indexOfSelectedCategory refers to the IndexPath of the
    //row in the TableView contained in THIS viewcontroller whose category
    //of coins we are modifying in this method
    //
    //Return value: a boolean that indicates whether a single coin has
    //been deleted - meaning that the user should return to the parentviewcontroller
    if 0 < indexOfSelectedCategory.row && indexOfSelectedCategory.row < self.coinsByCategory.count && self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.hasCoin(c: c) == true
    
        //the index is valid as it refers to a category in the coinsByCategory array
        //and the examined category has the coin in question
        if self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.count == 1
        
            //the coin "c" that we are going to delete is the only coin in the entire category
            //we reduce the problem to a simpler one that has been already solved (thanks mathematicians!)
            self.deleteCoinCategory(rowPath: indexOfSelectedCategory)
            
            return true
        
        else
        
            //there is more than one coin in the category
            self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.removeCoin(c: c)
            
            //we save the changes in the database...
            if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
            
                appDelegate.saveContext()
            
            
            return false
        
    
    
    return false


func addCoin(coinToAdd: Coin)

    //we check over each category to see if the coin can be added
    var addedToExistingCategory: Bool = false
    
    if let appDelegate = UIApplication.shared.delegate as? AppDelegate
    
        for i in 0..<self.coinsByCategory.count
        
            if self.coinsByCategory[i].coinCategory?.coinFitsCategory(aCoin: coinToAdd) == true
            
                //we can add the coin to the category
                self.coinsByCategory[i].coinCategory = CoinCategory(coins: self.coinsByCategory[i].coinCategory!.coinsInCategory+[coinToAdd], categoryType: coinsByCategory[i].coinCategory!.currentCategoryType)
                addedToExistingCategory = true
                break
            
        
    
        if addedToExistingCategory == false
        
            //since the coinToAdd does not fall in the existing categories, we create a new one
            let newCategory = CoinCategoryMO(context: appDelegate.persistentContainer.viewContext)
        
            newCategory.coinCategory = CoinCategory(coins: [coinToAdd], categoryType: CoinCategory.CategoryTypes.getTheCategoryFromString(str: UserDefaults.standard.object(forKey: "currentSortingCriteria") as! NSString).rawValue)
            
            //this index indicates that we are going to insert this newCategory into index "0" of all the categories in the table
            newCategory.index = 0
        
        
        appDelegate.saveContext()
        
        //now since we have added the coin, we now updated the indices of each CoinCategoryMO object
        updateIndices()
    


func coinFitsExistingCategory(coin: Coin) -> Bool

    //this function checks if the coin can be added to the existing categories
    for i in 0..<self.coinsByCategory.count
    
        if self.coinsByCategory[i].coinCategory?.coinFitsCategory(aCoin: coin) == true
        
            //we can add the coin to the category
            return true
        
    
    
    return false


func resortCoinsInNewCategories(newCategorySetting : CoinCategory.CategoryTypes?)

    //we want to resort all the coins in the category by new sorting criteria
    if newCategorySetting != nil && newCategorySetting! != CoinCategory.CategoryTypes.getTheCategoryFromString(str: UserDefaults.standard.object(forKey: "currentSortingCriteria") as! NSString)

    
        //We have a valid CoinCategory.CategoryTypes sorting criteria that is different from the one currently used.
        //We resort the coins in the collection by the new category
        UserDefaults.standard.setValue(newCategorySetting!.rawValue, forKey: "currentSortingCriteria")
        
        if self.coinsByCategory.count != 0
        
            //we actually have some coins to resort... let's get to work!
            self.isCurrentlyResortingCoinsIntoNewCategories = true
        
            //we first get an array of all the coins in existing categories
            var allCoinsArray : [Coin] = []
        
            for i in 0..<self.coinsByCategory.count
            
                allCoinsArray += self.coinsByCategory[i].coinCategory!.coinsInCategory
            
        
            //now we need to delete all the categories in existence...
            let firstCategoryIndexPath = IndexPath(row: 0, section: 0)
            let numberOfCategoriesToDelete = self.coinsByCategory.count
        
            for _ in 0..<numberOfCategoriesToDelete
            
                self.deleteCoinCategory(rowPath: firstCategoryIndexPath)
            
        
            //OK... now that we have deleted all old categories... it is time to start to create new ones...
            for i in 0..<allCoinsArray.count
            
                //AND we add the coin to the array!
                //this function also automatically updates the indices, so it is not an issue there
                self.addCoin(coinToAdd: allCoinsArray[i])
            
        
            //we are done resorting
            self.isCurrentlyResortingCoinsIntoNewCategories = false
        
    


private func updateIndices()

    //this function updates the "index" property so that
    //each CoinCategoryMO object in the coinsByCategory array
    //has an index corresponding to its position.
    //After this function is called, we must save the core data in the AppDelegate.
    //
    //This function is called ONLY after the changes to the CoinCategoryMO objects
    //are saved in core data and the self.coinsByCategory array is updated to have
    //the latest version of the data
    for i in 0..<self.coinsByCategory.count
    
        //the only reason why we create an entirely new CoinCategory object
        //is that the creation of an entirely new CoinCategory object
        //is the only way that the appDelegate will save the information
        self.coinsByCategory[i].index = Int16(i)
    
    
    if let appDelegate = UIApplication.shared.delegate as? AppDelegate
    
        appDelegate.saveContext()
    


//these delegate methods control the core data database
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)

    tableView.beginUpdates()


func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)

    switch type
    
    case .insert :
        if let newIndexPath = newIndexPath
        
            tableView.insertRows(at: [newIndexPath], with: .fade)
        
        
    case .delete:
        if let indexPath = indexPath
        
            tableView.deleteRows(at: [indexPath], with: .fade)
        
            
    case .update:
        if let indexPath = indexPath
        
            tableView.reloadRows(at: [indexPath], with: .fade)
        
        
    default:
        tableView.reloadData()
    
    
    if let fetchedObjects = controller.fetchedObjects
    
        self.coinsByCategory = fetchedObjects as! [CoinCategoryMO]
    


func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)

    tableView.endUpdates()
    
    if self.isCurrentlyResortingCoinsIntoNewCategories != true
    
        //we let the user know if the collection is empty
        if self.coinsByCategory.count == 0
        
            self.messageUserIfCollectionEmpty()
        
        else
        
            self.activateCollectionEmptyLabel(newState: false)
        
    

然后我的 CoinTableViewCell 类是:

//  Represents a cell of the coin buttons

import UIKit

class CoinTableViewCell: UITableViewCell 

@IBOutlet var countryLabel: UILabel!
@IBOutlet var valueAndDenominationLabel: UILabel!
@IBOutlet var quantityLabel: UILabel!
@IBOutlet var denominationOnlyLabel : UILabel!
@IBOutlet var yearLabel : UILabel!

override func awakeFromNib() 
    super.awakeFromNib()
    // Initialization code


func restoreAllLabelsToCell()

    //this is a function that is called when this cell is being initialized in the cellForRowAt method in a tableview..
    //we want to make all the labels visible so that the previous usage of a reusable tableview cell does not affect this usage of the cell
    countryLabel.isHidden = false
    valueAndDenominationLabel.isHidden = false
    quantityLabel.isHidden = false
    denominationOnlyLabel.isHidden = false
    yearLabel.isHidden = false


func configureLabelsForCategoryType(theType : NSString)

    //in this function, we remove all the extra labels
    //that contain information that does not relate to the general type of the category from the stack view
    //For example, the year label is removed when the category is a country, as a year does not determine what category a coin falls into.
    
    //we restore all the labels in this cell as we do not want the reusable cell's past usage
    //which may have lead to a label dissappearing to carry over into this new usage of the cell
    self.restoreAllLabelsToCell()
    
    switch theType
    
    case CoinCategory.CategoryTypes.COUNTRY_VALUE_AND_CURRENCY.rawValue:
        //we do not need information about the coin's denomination (without its value) or the year
        denominationOnlyLabel.isHidden = true
        yearLabel.isHidden = true
        
        //after we remove the labels, we now make the first label bold and black
        valueAndDenominationLabel.font = UIFont.boldSystemFont(ofSize: self.valueAndDenominationLabel.font.pointSize)
        valueAndDenominationLabel.textColor = UIColor.black
        
    case CoinCategory.CategoryTypes.COUNTRY.rawValue:
        //we do not need the information about the coin's value and denominations nor year
        valueAndDenominationLabel.isHidden = true
        denominationOnlyLabel.isHidden = true
        yearLabel.isHidden = true
        
        //after we remove the labels, we make the first label bold and black
        countryLabel.font = UIFont.boldSystemFont(ofSize: self.countryLabel.font.pointSize)
        countryLabel.textColor = UIColor.black
        
    case CoinCategory.CategoryTypes.CURRENCY.rawValue:
        //we do not information about the coin's value & denomination (together, that is), or year
        valueAndDenominationLabel.isHidden = true
        yearLabel.isHidden = true
        
        //after we remove the labels, we make the first label bold and black
        denominationOnlyLabel.font = UIFont.boldSystemFont(ofSize: self.denominationOnlyLabel.font.pointSize)
        denominationOnlyLabel.textColor = UIColor.black
        
    case CoinCategory.CategoryTypes.YEAR.rawValue:
        //we do not information about the coin's value, denomination, or country
        valueAndDenominationLabel.removeFromSuperview()
        denominationOnlyLabel.isHidden = true
        countryLabel.isHidden = true
        
        //after we remove the labels, we make the first label bold and black
        yearLabel.font = UIFont.boldSystemFont(ofSize: self.yearLabel.font.pointSize)
        yearLabel.textColor = UIColor.black
        
    default:
        //the string does not match any of the categories available
        //we do not remove any labels
        break
    



我的 CoreData 实现是 CoinCategoryMO 对象的集合,这些对象具有“索引”属性(用于它们在 uitableviewcontroller 中的位置)和一个包含 Coin 类对象的 CoinCategory 对象。

我已经尝试调试了好几天了,但我不知道出了什么问题。有人可以帮忙吗?

非常感谢,祝您有美好的一天!

【问题讨论】:

【参考方案1】:

我猜,category.coinCategory?.currentCategoryType 中的 CategoryTypesearchResults / coinsByCategory 中的所有对象中都不相同。

即第二个和第三个对象有COUNTRY_VALUE_AND_CURRENCY 作为它的currentCategoryType,第四个和第五个有COUNTRY

您能否确认currentCategoryType 对于searchResults / coinsByCategory 中的所有对象都相同?

【讨论】:

您好,感谢您的帮助。是的,我可以确认 searchResults / coinByCategory 中的所有对象的 currentCategoryType 都是相同的,因为我在初始化类别常量后在 cellForRowAt 方法的开头放入了print("DEBUG: the category's current category type is: \(category.coinCategory?.currentCategoryType)") 语句。在我使用类别中的硬币并看到与我在问题中发布的相同图形结果后,我检查了控制台,所有类别都具有相同的当前类别类型。 :-(。我现在不知道该怎么办。有什么想法吗?谢谢! 有时,即使我将新硬币添加到新类别中,tableview 的第一行中也有一个未填充的单元格,缺少一些标签。 当我使用“按国家、价值和面额排序”排序标准时,似乎出现了 tableview 控制器行的这个问题。【参考方案2】:

我想我现在知道错误是什么了:

在我的CoinTableViewCell 类的configureLabelsForCategoryType 方法中,我在valueAndDenomination 标签上调用了removeFromSuperView() 方法,这具有从单元格中永久删除该标签的效果。

因此该单元格没有该标签,即使它后来被用于另一个 CoinCategory。

我将valueAndDenominationLabel.removeFromSuperView() 行替换为valueAndDenominationLabel.isHidden = true,其效果是对用户隐藏valueAndDenominationLabel,但不会将其从单元格中永久删除。

非常感谢那些花时间阅读我的问题并做出回应的人。 你的努力帮助我思考了我的问题并找到了答案!

谢谢!

【讨论】:

以上是关于UITableViewController 的单元格内容消失的主要内容,如果未能解决你的问题,请参考以下文章

UITableViewController - 单元格大小分隔符和背景色

回退时 UITableViewController 单元格保持突出显示

如何将单元格插入 UITableViewController

UITableViewController 的单元格内容消失

在 UITableViewController 中重建表格单元格

自定义 UITableViewController 单元格 segue 不推送