Say I have a collectionView, using a UICollectionViewDiffableDataSource<Section, Row> where Section is
enum Section {
case info
}
and Row is
enum Row {
case name(name: String)
case description(description: String)
}
I then define a configuration for these cells
let textRegistration = UICollectionView.CellRegistration<TextInputCollectionViewCell, Row> { (cell, indexPath, item) in
cell.item = item
}
I have a delegate method setup to be notified when the content of these cells change from user input,
func nameDidChange(to newName: String?) {
self.navigationItem.title = newTitle
//Update my model
}
func descriptionDidChange(to newDescription: String?) {
//Update my model
}
Now here is my issue, if I initially add this data to my dataSource like this:
var infoSnapshot = self.dataSource.snapshot()
infoSnapshot.appendSections([.info])
infoSnapshot.appendItems([
.name(name: self.myModel.name),
.description(description: self.myModel.description)
], toSection: .info)
self.dataSource.apply(infoSnapshot, animatingDifferences: false)
As soon as this cell is offscreen and reset, once it comes back it uses the old data initially set in the method above.
How do I reset the data, if doing so would change the hash, messing with my diffable datasource? Should I set the data elsewhere, not tying it to the dataSource?
Before Editing
During Editing
After Editing
Related
I am working with DiffableDataSource inside of a collection view for the first time, but I am running into an error while trying to update a view.
So I am working with 2 custom cells. CustomCell1 is a cell that has a Label on the left and a UITextField on the right. CustomCell2 is a cell that has 2 column UIPickerView inside of it. CustomCell2 has a delegate that will let my main view controller know when it has been updated. This is all working fine.
So the problem is coming when I am trying to update the textfield in CustomCell1 with the value selected in the UIPickerView from CustomCell2. As a note, the textfield is being used for text entry in rows of the page. And the picker view is shown when a cell is clicked, and hidden when the cell is clicked again.
So in my delegate to update the cell, I have this code:
func updateSelectedCharacter(withCharacter character: String, indexPath: IndexPath) {
print(character)
DispatchQueue.main.async {
guard let charItem = self.dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)), let parentId = charItem.parentId else { return }
self.updatePlayerData(forPlayerIndex: parentId, character: character)
var snap = self.dataSource.snapshot()
snap.reloadItems([charItem])
self.dataSource.apply(snap)
}
}
And in my cellForRowAt, I have the following code to update the text field in the character cell (CustomCell1). The gameInfo object is a struct that has information, including the character name I am trying to display, that I will eventually store off into CoreData from the entries in the cells.
guard let parentId = info.parentId else { return UICollectionViewListCell() }
let playerData = self.gameInfo.playerData[parentId]
var obj = TextEntryCellInformation(title: rowTitle, rowType: info.rowType)
obj.value = obj.value = playerData?.character
obj.isSelectable = false
return collectionView.dequeueConfiguredReusableCell(using: textEntryCell, for: indexPath, item: obj)
And here is the cell registration for that cell, where TextEntryCellInformation holds the row type, title for the label, and an optional value that can set textfield's text:
private func createTextEntryListCellRegistration() -> UICollectionView.CellRegistration<TextEntryCollectionViewCell, TextEntryCellInformation> {
return UICollectionView.CellRegistration<TextEntryCollectionViewCell, TextEntryCellInformation> { cell, indexPath, data in
var config = TextEntryContentConfiguration()
config.title = data.title
config.tag = data.rowType.rawValue
config.textChangedDelegate = self
config.isSelectable = data.isSelectable
config.customPlaceholder = "required"
if let val = data.value {
config.textValue = val
}
cell.contentConfiguration = config
}
}
So now, I think I have explained most of the code. Whenever I select the value in the picker view, the first time it will show the value correctly in the character cell. But if I change the value, the text seems to disappear from the character cell. However, I am getting the correct value from the picker view, verified with the print call at the start of the delegate function. I am also verifying that after I apply in the update delegate function, I am running through the cell registration and the correct value is being assigned to the config.textValue. I am not sure why then, if the config is being data is being set as expected with the correct character name, why the UI is not updating to show that information.
I included what I think is the relevant code and information. However, if more is needed, I will definitely update.
Thanks in advance for any help given!
Turns out I found the answer. When setting the configuration on the content view of the textfield custom cell (CustomCell1), I was doing check to conform to equatable:
static func == (lhs: TextEntryContentConfiguration, rhs: TextEntryContentConfiguration) -> Bool {
return lhs.title == rhs.title
}
I needed to be doing:
static func == (lhs: TextEntryContentConfiguration, rhs: TextEntryContentConfiguration) -> Bool {
return lhs.textValue == rhs.textValue
}
This checks the difference to know if I should be updating the config values or not.
I've been trying to figure out a problem in my code but no luck finding a solution.
I have a dropdown library which I'm using for dropdown the library is ios Dropdown
1: https://github.com/AssistoLab/DropDown here in this I've customised the the tableViewCell and I've added a radio button. But when I'm trying to reload it its not updating its state it still remains the same even after setting the state.
func updateCellBy(text: String, index: Int, isSelected: Bool) {
self.index = index
radioButton.isSelected = isSelected
titleLabel.text = text
checkboxButtonWidthConstraint.constant = 20
radioButtonLeadingConstraint.constant = 15
}
But when I scroll this dropdown/tableview dropdown buttons state gets updated. I'm still wodering whats wrong with this. Tested if the data coming in the above method is correct or not but that is correct.DroppDown image
Note this dropdown is inside a collectionViewCell which is indeed inside a view.
dropDown.cellNib = UINib(nibName: "TableViewCell", bundle: nil)
dropDown.customCellConfiguration = { [self] (index: Index, item: String, cell: DropDownCell) -> Void in
guard let cell = cell as? TableViewCell else { return }
cell.updateCellBy(text: DropdownArrays.shared.getData()[index], index: index, isSelected: self.cellIndexes.contains(index))
cell.delegate = self
}
dropDown.dataSource = DropdownArrays.shared.getData()
above is how I'm configuring that drop down. Its just a tableview with cells.
for reloadind the drop down I'm calling the below method
dropDown.reloadAllComponents()
which is indeed reloading the cell inside the DropDown library
Reloads all the cells.
It should not be necessary in most cases because each change to
`dataSource`, `textColor`, `textFont`, `selectionBackgroundColor`
and `cellConfiguration` implicitly calls `reloadAllComponents()`.
*/
public func reloadAllComponents() {
DispatchQueue.executeOnMainThread {
self.tableView.reloadData()
self.setNeedsUpdateConstraints()
}
}
but the cell data is not updating.
When a user swipe a cell, it becomes possible to delete it. However, the deletion occurs without animation.
Part of code in my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
guard let foodCategoryDetailViewModel = foodCategoryDetailViewModel else { return }
tableView.delegate = nil
tableView.dataSource = nil
foodCategoryDetailViewModel.foodsInSelectedCategory
.bind(to: tableView.rx.items(cellIdentifier: FoodCategoryDetailTableViewCell.cellIdentifier, cellType: FoodCategoryDetailTableViewCell.self))
{ row, food, cell in
cell.foodCategoryDetailCellViewModel = foodCategoryDetailViewModel.cellViewModel(forRow: row)
}.disposed(by: disposeBag)
tableView.rx.itemDeleted.subscribe(onNext: { indexPath in
foodCategoryDetailViewModel.removeFoodFromApplication(atRow: indexPath.row)
}).disposed(by: disposeBag)
}
Part of code in my ViewModel:
class FoodCategoryDetailTableViewViewModel: FoodCategoryDetailTableViewViewModelType {
var foodsInSelectedCategory: BehaviorRelay<[Food]>
func removeFoodFromApplication(atRow row: Int) {
if let food = getFood(atRow: row) {
foodsInSelectedCategory.remove(at: row)
//remove from core data
CoreDataHelper.sharedInstance.removeFoodFromApplication(foodName: food.name!)
}
}
How to animating deleting process from tableView?
In order to animate the deleting process, you need a datasource that is designed to do that. The default datasource in RxSwift is not.
The most popular library for such a thing is RxDataSources which brings in a full blown multi-section system for animating table and collection views, but if you don't want something that elaborate, you can easily write your own.
Here is an example of a simple animatable datasource for RxSwift that uses DifferenceKit to calculate which cells must be animated on/off: (https://github.com/danielt1263/RxMultiCounter/blob/master/RxMultiCounter/RxExtensions/RxSimpleAnimatableDataSource.swift)
In my mobile application I would like to update the tableView datasource by a pull to refresh request, but I don't know how to insert the new items on top of the tableview datasource.
I see that there is a a method of insertRows such as : self.tableView?.insertRows(at: [indexPath], with: .top) but how do I add the newItems here according to my methods I have?
I have a function called initializedTableView() that initializes the tableView with PublishSubject observable items.
func initializeTableView() {
viewModel
.items
.subscribe(onNext: { items in
self.tableView?.delegate = nil
self.tableView?.dataSource = nil
Observable.just(items)
.bind(to:(self.tableView?.rx.items(cellIdentifier:
itemCell.Identifier, cellType: itemCell.self))!) {
(index, element, cell) in
cell.itemModel = element
}.disposed(by: self.disposeBag)
})
.disposed(by: disposeBag)
}
This function is called once a pull to refresh is requested by user:
func refreshTableView() {
// get new items
viewModel
.newItems
.subscribe(onNext: { newItems in
//new
let new = newItems.filter({ item in
// items.new == true
})
//old
var old = newItems.filter({ item -> Bool in
// items.new == false
})
new.forEach({item in
// how to update tableView.rx.datasource here???
})
}).disposed(by: disposeBag)
}
struct ViewModel {
let items: BehaviorRelay<[Item]>
init() {
self.items = BehaviorRelay(value: [])
}
func fetchNewItems() {
// This assumes you are properly distinguishing which items are new
// and `newItems` does not contain existing items
let newItems: [Item] = /* However you get new items */
// Get a copy of the current items
var updatedItems = self.items.value
// Insert new items at the beginning of currentItems
updatedItems.insert(contentsOf: newItems, at: 0)
// For simplicity this answer assumes you are using a single cell and are okay with a reload
// rather than the insert animations.
// This will reload your tableView since 'items' is bound to the tableView items
//
// Alternatively, you could use RxDataSources and use the `RxTableViewSectionedAnimatedDataSource`
// This will require a section model that conforms to `AnimatableSectionModelType` and some
// overall reworking of this example
items.accept(updatedItems)
}
}
final class CustomViewController: UIViewController {
deinit {
disposeBag = DisposeBag()
}
#IBOutlet weak var tableView: UITableView!
private var disposeBag = DisposeBag()
private let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomTableCell.self, forCellReuseIdentifier: "ReuseID")
tableView.refreshControl = UIRefreshControl()
viewModel.items
.bind(to: tableView.rx.items(cellIdentifier: "ReuseID", cellType: CustomTableCell.self)) { row, item, cell in
// Configure cell with item
cell.configure(with: item)
}
.disposed(by: disposeBag)
tableView.refreshControl?.rx.controlEvent(.valueChanged)
.subscribe(onNext: { [weak self] in
self?.viewModel.fetchNewItems()
})
.disposed(by: disposeBag)
}
}
Alternative answer using BehaviorRelay and bindings. This way, you are only updating the items relay and it will automatically update the tableView. It also provides a more "Rx" way of handling pull to refresh.
As mentioned in the code comments, this assumes you are determining which items are new and that newItems does not contain any existing items. Either way this should provide a starting point.
struct ViewModel {
let items: Observable<[Item]>
init(trigger: Observable<Void>, newItems: #escaping () -> Observable<[Item]>) {
items = trigger
.flatMapLatest(newItems)
.scan([], accumulator: { $1 + $0 })
}
}
The above doesn't handle errors, nor does it handle resets, but the scan will put the new items at the top of the list.
The situation doesn't feel right though. Normally, the API call returns all the items, how can it possibly know which items are "new"?
I did something similar with my app since I had issues with tableView.insertRows.
Here is the code:
func loadMoreComments() {
// call to backend to get more comments
getMoreComments { (newComments) in
// combine the new data and your existing data source array
self.comments = newComments + self.comments
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
// calculate the total height of the newly added cells
var addedHeight: CGFloat = 0
for i in 0...result.count {
let indexRow = i
let tempIndexPath = IndexPath(row: Int(indexRow), section: 0)
addedHeight = addedHeight + self.tableView.rectForRow(at: tempIndexPath).height
}
// adjust the content offset by how much height was added to the start so that it looks the same to the user
self.tableView.contentOffset.y = self.tableView.contentOffset.y + addedHeight
}
}
So, by calculating the heights of the new cells being added to the start and then adding this calculated height to the tableView.contentOffset.y, I was able to add cells to the top of the tableView seamlessly without reworking my tableView. This may look like a jerky workaround, but the shift in tableView.contentOffset isn't noticeable if you calculate the height properly.
I am making a coin collection app which is supposed to help users maintain a portable record of their collection. There are two view controllers so far: the CoinTableViewController which presents a tableview of all the categories of coins, and the CoinCategoryViewController which is supposed to have a table view of the specific coins in this category.
I obviously want my coins to be reportable based on multiple criteria (such as the same year, same country, etc. and etc.). To this effect, I have created a reusable tableview cell subclass called CoinTableViewCell that has 5 UILabels which represent all the information that the coins into the collection can be grouped into categories by.
Here is what my storyboard looks like
The logic is that based on my current sorting criteria, I can hide certain labels from the cell to reflect the criteria that the coins were sorted by. The settings wheel opens up a menu that appears on the left side of the screen with options for how to sort the coins, and the closing of the menu makes the Coin Categories controller resort the coins in the collection if the sorting option was changed.
My problem is that while my program works overall, sometimes, some of the cells do not appear completely after the program resorts them (after the user opens up the menu and selects an option).
Here's what this looks like:
As you may see, the bottom two cells in the view controller are missing the top label even though the other cells have them. And since I have implemented the tableview controller's cells as being resizable, the table view cells should automatically resize themselves to fit the content inside.
Here is my code:
// 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)
}
}
}
And then my CoinTableViewCell class is:
// 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
}
}
}
My CoreData implementation is a collection of CoinCategoryMO objects which have the property "Index" (for their position in the uitableviewcontroller) and a CoinCategory object which holds objects of the Coin Class.
I have been trying to debug this for several days now, and I have no idea what is going wrong. Could anyone please help?
Many many thanks in advance, and have a great day!
I guess, CategoryType in category.coinCategory?.currentCategoryType is not same across all objects in searchResults / coinsByCategory.
i.e. 2nd and 3rd objects has COUNTRY_VALUE_AND_CURRENCY as its currentCategoryType and 4th and 5th has COUNTRY.
Can you confirm that currentCategoryType is same for all objects in searchResults / coinsByCategory?
I think I know what the error is now:
In my configureLabelsForCategoryType method for my CoinTableViewCell class, I had invoked the removeFromSuperView() method on the valueAndDenomination label, which had the effect of permanently removing that label from the cell.
Thus the cell did not have that label, even if it was later reused for another CoinCategory.
I replaced the valueAndDenominationLabel.removeFromSuperView() line with valueAndDenominationLabel.isHidden = true which has the effect of hiding the valueAndDenominationLabel from the user, but not permanently removing it from the cell.
Many many thanks to those who took the time to read my question and to respond.
Your efforts helped me think about my problem and track it down!
Thank you!