I have a UICollectionView which implements pages organized into sections. I want to split a section into two parts and have the following logic. But UICollectionView doesn't seem to be happy with this:
func splitSection(at index: IndexPath) {
// this performs the logical split
self.document.splitSection(at: index)
if let cv = self.collectionView {
cv.performBatchUpdates({
let N = self.document.numberOfSection
let n = N - index.section - 1
let range = Range.init(uncheckedBounds: (index.section, n))
let affectedSections = IndexSet.init(integersIn: range)
cv.reloadSections(affectedSections)
let sections = IndexSet.init(integersIn: Range.init(uncheckedBounds: (N-1, 1)))
cv.insertSections(sections)
}, completion: { (_) in
// commit section updates
})
}
}
in numberOfSections function return 2
then in cellforitemat function use switch on indexpath.section
Ok. So I think I have solved the problem after various experimentation. Here is the final solution in case any one else gets into this problem.
The basic idea is:
Move all sections after the the section being split by 1
Insert a section after the section being split
Delete all the items after the split index from the current section
Insert all the items into the newly created section.
The reverse idea can be employed for merging the sections.
Here is the solution:
func splitSection(at index: IndexPath) {
# first get a list of deleted and inserted indices as a result of splitting
let (deleted, inserted) = self.document.splitSection(at: index)
if let cv = self.collectionView {
cv.performBatchUpdates({
let N = self.document.numberOfSection
// now move over the sections to make space for one more section.
for idx in index.section+1..<N-1 {
cv.moveSection(idx, toSection: idx+1)
}
// add a new section
cv.insertSections(IndexSet.init(integer: index.section+1))
// now perform item movements to finish splitting
cv.deleteItems(at: deleted)
cv.insertItems(at: inserted)
}, completion: { (_) in
self.document.updateSections()
})
}
}
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.
A Section may contain 1 header, many content items and 1 footer.
For DiffableDataSource, most of the online examples, are using enum to represent Section. For instance
func applySnapshot(_ animatingDifferences: Bool) {
var snapshot = Snapshot()
snapshot.appendSections([.MainAsEnum])
snapshot.appendItems(filteredTabInfos, toSection: .MainAsEnum)
dataSource?.apply(snapshot, animatingDifferences: animatingDifferences)
}
However, when the Section has a dynamic content footer, we may need to use struct to represent Section. For instance
import Foundation
struct TabInfoSection {
// Do not include content items [TabInfo] as member of Section. If not, any mutable
// operation performed on content items, will misguide Diff framework to throw
// away entire current Section, and replace it with new Section. This causes
// flickering effect.
var footer: String
}
extension TabInfoSection: Hashable {
}
But, how are we suppose to update only footer?
The current approach provided by
DiffableDataSource: Snapshot Doesn't reload Headers & footers is not entirely accurate
If I try to update footer
class TabInfoSettingsController: UIViewController {
…
func applySnapshot(_ animatingDifferences: Bool) {
var snapshot = Snapshot()
let section = tabInfoSection;
snapshot.appendSections([section])
snapshot.appendItems(filteredTabInfos, toSection: section)
dataSource?.apply(snapshot, animatingDifferences: animatingDifferences)
}
var footerValue = 100
extension TabInfoSettingsController: TabInfoSettingsItemCellDelegate {
func crossButtonClick(_ sender: UIButton) {
let hitPoint = (sender as AnyObject).convert(CGPoint.zero, to: collectionView)
if let indexPath = collectionView.indexPathForItem(at: hitPoint) {
// use indexPath to get needed data
footerValue = footerValue + 1
tabInfoSection.footer = String(footerValue)
//
// Perform UI updating.
//
applySnapshot(true)
}
}
}
I will get the following flickering outcome.
The reason of flickering is that, the diff framework is throwing entire old Section, and replace it with new Section, as it discover there is change in TabInfoSection object.
Is there a good way, to update footer in Section via DiffableDataSource without causing flickering effect?
p/s The entire project source code can be found in https://github.com/yccheok/ios-tutorial/tree/broken-demo-for-footer-updating under folder TabDemo.
Have you thought about making a section only for the footer? So that way there's no reload, when it flickers, since it's technically not apart of the problematic section?
There is a fast fix for it, but you will loose the animation of the tableview. In TabInfoSettingsController.swift you can force false the animations in this function:
func applySnapshot(_ animatingDifferences: Bool) {
var snapshot = Snapshot()
let section = tabInfoSection;
snapshot.appendSections([section])
snapshot.appendItems(filteredTabInfos, toSection: section)
dataSource?.apply(snapshot, animatingDifferences: false)
}
You will not see the flickering effect but you will loose the standard animation.
if you want to update only collectionview footer text then make it variable of TabInfoSettingsFooterCell.
var tableSection: TabInfoSettingsFooterCell?
DataSource
func makeDataSource() -> DataSource {
let dataSource = DataSource(
collectionView: collectionView,
cellProvider: { (collectionView, indexPath, tabInfo) -> UICollectionViewCell? in
guard let tabInfoSettingsItemCell = collectionView.dequeueReusableCell(
withReuseIdentifier: TabInfoSettingsController.tabInfoSettingsItemCellClassName,
for: indexPath) as? TabInfoSettingsItemCell else {
return nil
}
tabInfoSettingsItemCell.delegate = self
tabInfoSettingsItemCell.reorderDelegate = self
tabInfoSettingsItemCell.textField.text = tabInfo.getPageTitle()
return tabInfoSettingsItemCell
}
)
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
guard kind == UICollectionView.elementKindSectionFooter else {
return nil
}
let section = dataSource.snapshot().sectionIdentifiers[indexPath.section]
guard let tabInfoSettingsFooterCell = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: TabInfoSettingsController.tabInfoSettingsFooterCellClassName,
for: indexPath) as? TabInfoSettingsFooterCell else {
return nil
}
tabInfoSettingsFooterCell.label.text = section.footer
//set tableSection value
self.tableSection = tabInfoSettingsFooterCell
return tabInfoSettingsFooterCell
}
return dataSource
}
TabInfoSettingsItemCellDelegate
func crossButtonClick(_ sender: UIButton) {
let hitPoint = (sender as AnyObject).convert(CGPoint.zero, to: collectionView)
if let indexPath = collectionView.indexPathForItem(at: hitPoint) {
footerValue = footerValue + 1
tabInfoSection.footer = String(footerValue)
//Update section value
self.tableSection?.label.text = String(footerValue)
}
}
I have a expandable tableview with custom headers. When I load the tableview I set the alpha for few sections based on if they have any items in it.
I set the alpha back to 1 once I have some items in those sections.
The problem I am facing is as soon as I click on any of the section to expand that section, it just resets the view for all headers that are below the clicked one and just removes the alpha. For Ex If I click on 1st one, all the headers become alpha 1 but if I click on the 4th one, just the 4th and 5th become alpha 1. For ex If I click on 1 this is how it shows -
In order to achieve the collapsable and expandable view, I use the following code to refresh the section -
let indexSet: IndexSet = IndexSet(integer: section)
self.filterTableView.beginUpdates()
self.filterTableView.reloadSections(indexSet, with: UITableView.RowAnimation.fade)
self.filterTableView.endUpdates()
The way I set the header is shown below -
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "FilterOptionHeaderView") as! FilterOptionHeaderView
let filterHeader = self.filterHeaders[section]
let selectedCount = self.selectedItems[filterHeader.key]?.count ?? 0
if selectedCount > 0 {
headerView.headerLabel.text = filterHeader.name + "(\(selectedCount))"
} else {
headerView.headerLabel.text = filterHeader.name
}
headerView.tag = kHeaderSectionTag + section
let headerTapGesture = UITapGestureRecognizer()
headerTapGesture.addTarget(self, action: #selector(sectionHeaderTapped(_:)))
headerView.addGestureRecognizer(headerTapGesture)
if self.sectionItems[section].isEmpty || self.sectionItems[section].count < 1 {
headerView.alpha = 0.5
} else {
headerView.alpha = 1
}
return headerView
}
I have tried setting the alpha in didEndDisplayingHeaderView as well but still doesn't work. Please let me know how to achieve this. I know that all the headers are reset because the arrow of the expanded direction also comes back to initial state even though i have rotation animation in place that shows up and then resets.
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!
I setup a TableView and fetching paginated data. For page 1, as it's hitting route without any parameter (e.g /?page=2). Now I am trying to implement infinite scroll kind load more. I am trying to make a call everytime it hits the 5th row from bottom.
What this code does is fetches non-stop as soon as I hit the 5th cell from bottom. I think it's because the table stays on the 5th cell from bottom and keep adding cells from bottom toward up (but it may be just visual illusion)
class TableView1: UITableViewController {
var currentPage: Int = 1
var results: [JSON]? = []
var urlEndpoint: String?
var isLoading = false
override func viewDidLoad() {
loadItems()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// connections to cell..
let rowsToLoadFromBottom = 5;
let rowsLoaded = self.results?.count // count of fetched items
if (!isLoading && (indexPath.row >= (rowsLoaded! - rowsToLoadFromBottom))) {
self.loadItems()
}
return cell
}
And loadItems() function:
func loadItems() {
isLoading = true
urlEndpoint = "http://appurl.app/feed"
if currentPage != 1 {
currentPage = currentPage! + 1
urlEndpoint = "http://appurl.app/feed?page=\(currentPage)"
// print(urlEndpoint)
}
// Alamofire call.. {
// Error handling
// Got results {
self.currentPage = json["current_page"].integer
self.currentPage = self.currentPage + 1
self.results = data
self.tableView.reloadData()
}
self.isLoading = false
}
}
Update: I made it fetch properly. Now, I am seeing the links with page parameters on console. However, now it's loading all pages in a sudden (I have 6 pages, so 6 of them) so it doesn't really stop appending and I can actually see like a loop.
Update 2:
self.currentPage = json["current_page"].int!
if self.currentPage == 1 {
self.results = data // data is json data fetched
self.tableView.reloadData()
} else {
var currentCount = self.results!.count;
let indxesPath : [NSIndexPath] = [NSIndexPath]()
for result in data {
self.results?.append(result)
currentCount++
}
self.tableView.insertRowsAtIndexPaths(indxesPath, withRowAnimation: UITableViewRowAnimation.Bottom)
}
self.currentPage = self.currentPage + 1
}
self.isLoading = false
But it's crashing on line self.tableView.insertRowsAtIndexPaths(indxesPath, withRowAnimation: UITableViewRowAnimation.Bottom)
The error: Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (40) must be equal to the number of rows contained in that section before the update (20), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
I have found my answer in another SO question. Here it is applied to my scenario
if self.currentPage == 1 {
self.results = data
self.tableView.reloadData()
} else {
var currentCount = self.results!.count;
var indxesPath : [NSIndexPath] = [NSIndexPath]()
for result in data {
indxesPath.append(NSIndexPath(forRow:currentCount,inSection:0));
self.results?.append(result)
currentCount++
}
self.tableView.insertRowsAtIndexPaths(indxesPath, withRowAnimation: UITableViewRowAnimation.Bottom)
}