How to reset iOS-Charts animation in tableview cell - ios

I am using a line chart from iOS-Charts in a custom tableview cell that expands on touch. I'm not loading the charts in cellForRow: since this loads all the charts in all the cells and kills scrolling performance, so I have it loading in didSelectRow:. The problem is that for some reason, the chart doesn't load the first time a user touches a cell and I have no idea why. Also, it seems like the chart animation starts when an expanded cell (that is currently showing a chart) is collapsed.
Here is my didSelectRow: and below is the charting func in my custom cell. I've been wrestling with this, any insight would be greatly appreciated!
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell:CandDetailTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as! CandDetailTableViewCell
// CALLS METHOD TO BUILD CHART WITH DATA I SAVED IN THESE ARRAYS
cell.setChart(dataForMonths, values: dataForRatings)
let previousIndexPath = selectedIndexPath
if indexPath == selectedIndexPath {
// if user taps cell that was already selected so it collapses
selectedIndexPath = nil
} else {
// when user selects new cell
selectedIndexPath = indexPath
}
var indexPaths:Array <NSIndexPath> = []
if let previous = previousIndexPath {
indexPaths += [previous]
}
if let current = selectedIndexPath {
indexPaths += [current]
}
if indexPaths.count > 0 {
tableView.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
}
tableView.scrollRectToVisible(tableView.rectForRowAtIndexPath(indexPath), animated: true)
}
Here is chart func in the tableview cell:
func setChart(dataPoints: [String], values: [Double]) {
var dataEntries: [ChartDataEntry] = []
for i in 0..<dataPoints.count {
let dataEntry = ChartDataEntry(value: values[i], xIndex: i)
dataEntries.append(dataEntry)
}
let lineChartDataSet = LineChartDataSet(yVals: dataEntries, label: "Rating")
let lineChartData = LineChartData(xVals: dataPoints, dataSet: lineChartDataSet)
analyticsView.data = lineChartData
analyticsView.xAxis.labelPosition = .Bottom
analyticsView.backgroundColor = UIColor.whiteColor()
analyticsView.userInteractionEnabled = false
analyticsView.drawGridBackgroundEnabled = false
analyticsView.drawBordersEnabled = false
analyticsView.leftAxis.enabled = false
analyticsView.rightAxis.enabled = false
analyticsView.xAxis.gridLineWidth = 0.0
analyticsView.xAxis.axisLineColor = UIColor.clearColor()
analyticsView.legend.enabled = false
// Removes the description text off the chart
analyticsView.descriptionText = ""
analyticsView.animate(xAxisDuration: 2.0, yAxisDuration: 2.0)
}

first, put analyticsView.data = lineChartData after you setup your chat view. It needs to read some properties to do some math;
second, when and where you add the chart view into the cell's content view?
Also, it seems a little strange when you call tableView.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic) in didSelectRow, I assume you should add the chart view into cell's content view here, not reload data? Because it will trigger another cellForItemAtIndexPath and override what you have done?
basically when you do addSubView, it will start drawing the chart. You can add a break point at addSubView and drawRect of the line chart view, and see if it can break when you hit the cell.
You can also manually trigger a redraw by chartView.setneedsDisplay
Update:
If you want to reset animation, call the animate API again and maybe another setNeedsDisplay

Related

How to update footer in Section via DiffableDataSource without causing flickering effect?

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)
}
}

piecharts (iOS charts) not displayed in custom tableviewcell

screenshot
I have a problem with piecharts not showing in my custom tableview cell.
The tableview cells are added through storyboard, each cell contains a small icon image, two labels and a UIView which I set in the view inspector as PieChartView.
The small icon image and the text in the two labels is showing fine, no problem there. For the piecharts however, I get no error message but simply none of the charts is displayed. The table shows up, is filled with the proper texts in the labels but the piechartview is empty except the middle hole of the piechart. When I click one of these, the piechartview is displayed - not totally correct (only one of the two pie slices is displayed, the other part is missing).
The touch event therefore makes the slice visible, but I want the piechart be visible for all cells upon running the cell for row function.
I have added the code for the tableviewcontroller and the tableviewcell. Would be great, if someone could point out my error. I have researched and tried a lot, among others the following stack overflow resources:
Charts not plotting in tableViewCell
iosChart not displaying LineChartView points in UITableViewCell
Adding a SubView to UITableViewCell doesn't get displayed
How to implement iOS Chart in a tableview cell?
The screenshot shows the situation after I click a few of the invisible piecharts. They become visible, also when I then scroll down the table some more (not all) of piecharts in random cells are visible, some are not.
Code for tableviewcontroller:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "easyAndDiffAllWordsCell", for: indexPath) as! BVBResultsDiffAndEasyAllWordsGraphTableViewCell
let easyVoc = parsedInEasyVocStructures[indexPath.row]
//get the current voc for the writings
let currentVoc = BVBVocabularyManager.getVoc()
cell.label1.text = currentVoc.kanji
cell.label2.text = currentVoc.kanji2
let image = UIImage(named: "plus")
cell.plusMinusImage.image = image
//set the percentages
cell.percentageSolved = arrayOfSuccessPercentagesForPieChart
cell.percentageNotSolved = arrayOfNegativeSuccessPercentagesForPieChart
cell.setChart(forIndexNo:indexPath.row, dataPoints: months, valuesSolved: arrayOfSuccessPercentagesForPieChart, valuedNonSolved: arrayOfNegativeSuccessPercentagesForPieChart)
cell.setNeedsDisplay()
cell.pieChartView.clipsToBounds = true
cell.pieChartView.layer.masksToBounds = true
cell.pieChartView.contentMode = .scaleAspectFit
return cell
}
And for the tableViewCell:
class BVBResultsDiffAndEasyAllWordsGraphTableViewCell: UITableViewCell {
#IBOutlet weak var kanjiL: UILabel!
#IBOutlet weak var translationL: UILabel!
#IBOutlet weak var pieChartView: PieChartView!
#IBOutlet weak var plusMinusImage: UIImageView!
var testconditions: Array<String>?
var percentageSolved: Array<Int>?
var percentageNotSolved: Array<Int>?
var solvedPercentageDataEntry = PieChartDataEntry(value: 0)
var nonSolvedPercentageDataEntry = PieChartDataEntry(value: 0)
var percentageSolvedNonSolvedDataEntries = [PieChartDataEntry]()
override func awakeFromNib() {
super.awakeFromNib()
solvedPercentageDataEntry.label = NSLocalizedString("solved", comment: "piechart label for the solved area")
pieChartView.chartDescription?.text = ""
pieChartView.legend.enabled = false
pieChartView.setExtraOffsets(left: 2, top: 0, right: 2, bottom: 0)
pieChartView.holeRadiusPercent = 2.8
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
//not used
}
func setChart(forIndexNo: Int, dataPoints: [String], valuesSolved: [Int], valuedNonSolved: [Int]){
var dataEntries: [PieChartDataEntry] = []
solvedPercentageDataEntry = PieChartDataEntry(value: Double(valuesSolved[forIndexNo]), label: "")
nonSolvedPercentageDataEntry = PieChartDataEntry(value:Double(valuedNonSolved[forIndexNo]), label: "")
dataEntries = [solvedPercentageDataEntry, nonSolvedPercentageDataEntry]
percentageSolvedNonSolvedDataEntries = [solvedPercentageDataEntry, nonSolvedPercentageDataEntry]
let pieChartDataSet = PieChartDataSet(entries: percentageSolvedNonSolvedDataEntries, label: nil)
pieChartDataSet.drawValuesEnabled = false
let pieChartData = PieChartData(dataSet: pieChartDataSet)
let colors = [UIColor.themeColor(), UIColor.red]
pieChartDataSet.colors = colors as! [NSUIColor]
pieChartView.data = pieChartData
pieChartView.notifyDataSetChanged()
}
}
I test your code. It works fine.
And the middle hole, maybe it's the configuration problem. Try to add this
pieChartDataSet.drawIconsEnabled = false
pieChartDataSet.sliceSpace = 1
pieChartDataSet.highlightColor = UIColor.white
pieChartDataSet.entryLabelColor = UIColor.white
pieChartDataSet.selectionShift = 0
pieChartView.holeRadiusPercent = 0.5
pieChartView.transparentCircleRadiusPercent = 0.0
Hope this helps

UITableViewController's Cell's Content disappearing

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!

UICollectionView cell reuse causing problems even after taking steps

I know this question has been asked many times.
I'm using a UICollectionView with custom cells which have a few properties, most important being an array of UISwitch's and and array of UILabel's. As you can guess, when I scroll, the labels overlap and the switches change state. I have implemented the method of UICollectionViewCell prepareForReuse in which I empty these arrays and reset the main label text.
I have tried to combine solutions from different answers and I have reached a point where my labels are preserved, but the state of my switches in the cells isn't. My next step was to create an array to preserve the state before removing the switches and then set the on property of a newly created switch to a value of this array at an index. This works, until after I scroll very fast and switches in cells which were not selected previously become selected(or unselected). This is creating a huge problem for me.
This is my collectionView cellForItemAtIndexPath method:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("pitanjeCell", forIndexPath: indexPath) as! PitanjeCell
// remove views previously created and create array to preserve the state of switches
var selectedSwitches: [Bool] = []
for item: UIView in cell.contentView.subviews {
if (item.isKindOfClass(UILabel) && !item.isEqual(cell.tekstPitanjaLabel)){
item.removeFromSuperview()
}
if (item.isKindOfClass(UISwitch)){
selectedSwitches.append((item as! UISwitch).on)
item.removeFromSuperview()
}
}
// get relevant data needed to place cells programmatically
cell.tekstPitanjaLabel.text = _pitanja[indexPath.row].getText()
let numberOfLines: CGFloat = CGFloat(cell.tekstPitanjaLabel.numberOfLines)
cell.setOdgovori(_pitanja[indexPath.row].getOdgovori() as! [Odgovor])
var currIndex: CGFloat = 1
let floatCount: CGFloat = CGFloat(_pitanja.count)
let switchConstant: CGFloat = 0.8
let switchWidth: CGFloat = cell.frame.size.width * 0.18
let heightConstant: CGFloat = (cell.frame.size.height / (floatCount + 2) + (numberOfLines * 4))
let labelWidth: CGFloat = cell.frame.size.width * 0.9
for item in _pitanja[indexPath.row].getOdgovori() {
// create a switch
let odgovorSwitch: UISwitch = UISwitch(frame: CGRectMake((self.view.frame.size.width - (switchWidth * 2)), currIndex * heightConstant , switchWidth, 10))
odgovorSwitch.transform = CGAffineTransformMakeScale(switchConstant, switchConstant)
let switchValue: Bool = selectedSwitches.count > 0 ? selectedSwitches[Int(currIndex) - 1] : false
odgovorSwitch.setOn(switchValue, animated: false)
// cast current item to relevant class
let obj: Odgovor = item as! Odgovor
// create a label
let odgovorLabel: UILabel = UILabel(frame: CGRectMake((self.view.frame.size.width / 12), currIndex * heightConstant , labelWidth, 20))
odgovorLabel.text = obj.getText();
odgovorLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
odgovorLabel.font = UIFont(name: (odgovorLabel.font?.fontName)!, size: 15)
// add to cell
cell.addSwitch(odgovorSwitch)
cell.addLabel(odgovorLabel)
currIndex++
}
return cell
}
My custom cell also implements methods addSwitch and addLabel which add the element to the contentView as a subview.
Is there any way I can consistently preserve the state of switches when scrolling?
EDIT: As per #Victor Sigler suggestion, I created a bydimensional array like this:
var _switchStates: [[Bool]]!
I initialized it like this:
let odgovori: Int = _pitanja[0].getOdgovori().count
_switchStates = [[Bool]](count: _pitanja.count, repeatedValue: [Bool](count: odgovori, repeatedValue: false))
And I changed my method like this:
for item: UIView in cell.contentView.subviews {
if (item.isKindOfClass(UILabel) && !item.isEqual(cell.tekstPitanjaLabel)){
item.removeFromSuperview()
}
if (item.isKindOfClass(UISwitch)){
_switchStates[indexPath.row][current] = (item as! UISwitch).on
current++
selectedSwitches.append((item as! UISwitch).on)
item.removeFromSuperview()
}
}
And in the end:
let switchValue: Bool = _switchStates.count > 0 ? _switchStates[indexPath.row][Int(currIndex) - 1] : false
First of all as you said in your question regarding the default behavior of the cell in the UICollectionView you need to save the state of each cell, in your case with the UISwitch, but you need to preserve the state in a local property, not in the cellForRowAtIndexPath method because this method is calles every time a cell is going to be reused.
So first declare the array where you going to save the state of the UISwitch's outside this function, something like this:
var selectedSwitches: [Bool] = []
Or you can declare it and then in your viewDidLoad instantiate it, it's up to you, I recommend you instantiate it in your viewDidLoad and only declare it as a property like this:
var selectedSwitches: [Bool]!
Then you can do whatever you want with the state of your UISwitch's always of course preserving when change to on or off.
I hope this help you.
I have done it!
In the end I implemented this:
func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
var current: Int = 0
var cell = cell as! PitanjeCell
for item: UISwitch in cell.getOdgovoriButtons() {
if(current == _switchStates[0].count) { break;}
_switchStates[indexPath.row][current] = (item).on
current++
}
}
In this function I saved the state.
This in combo with prepareForReuse:
public override func prepareForReuse() {
self.tekstPitanjaLabel.text = nil
self._odgovoriButtons.removeAll()
self._odgovoriLabels.removeAll()
self._odgovori.removeAll()
super.prepareForReuse()
}
Finally did it!

How to drag a static cell into tableView swift?

I have one tableView in my storyBoard where I added 4 static cell into it and my storyBoard look like:
I don't have any dataSource for this tableView because my cells are static.
And I use below code to drag a cell and it is working fine till I scroll a table.
import UIKit
class TableViewController: UITableViewController {
var sourceIndexPath: NSIndexPath = NSIndexPath()
var snapshot: UIView = UIView()
let longPress: UILongPressGestureRecognizer = {
let recognizer = UILongPressGestureRecognizer()
return recognizer
}()
override func viewDidLoad() {
super.viewDidLoad()
longPress.addTarget(self, action: "longPressGestureRecognized:")
self.tableView.addGestureRecognizer(longPress)
self.tableView.allowsSelection = false
}
override func viewWillAppear(animated: Bool) {
self.tableView.reloadData()
}
// MARK: UIGestureRecognizer
func longPressGestureRecognized(gesture: UILongPressGestureRecognizer){
let state: UIGestureRecognizerState = gesture.state
let location:CGPoint = gesture.locationInView(self.tableView)
if let indexPath: NSIndexPath = self.tableView.indexPathForRowAtPoint(location){
switch(state){
case UIGestureRecognizerState.Began:
sourceIndexPath = indexPath
let cell: UITableViewCell = self.tableView .cellForRowAtIndexPath(indexPath)!
//take a snapshot of the selected row using helper method
snapshot = customSnapshotFromView(cell)
//add snapshot as subview, centered at cell's center
var center: CGPoint = cell.center
snapshot.center = center
snapshot.alpha = 0.0
self.tableView.addSubview(snapshot)
UIView.animateWithDuration(0.25, animations: { () -> Void in
center.y = location.y
self.snapshot.center = center
self.snapshot.transform = CGAffineTransformMakeScale(1.05, 1.05)
self.snapshot.alpha = 0.98
cell.alpha = 0.0
}, completion: { (finished) in
cell.hidden = true
})
case UIGestureRecognizerState.Changed:
let cell: UITableViewCell = self.tableView.cellForRowAtIndexPath(indexPath)!
var center: CGPoint = snapshot.center
center.y = location.y
snapshot.center = center
print("location \(location.y)")
//is destination valid and is it different form source?
if indexPath != sourceIndexPath{
//update data source
//I have commented this part because I am not using any dataSource.
// self.customArray.exchangeObjectAtIndex(indexPath.row, withObjectAtIndex: sourceIndexPath.row)
//move the row
self.tableView.moveRowAtIndexPath(sourceIndexPath, toIndexPath: indexPath)
//and update source so it is in sync with UI changes
sourceIndexPath = indexPath
}
if (location.y < 68) || (location.y > 450) {
print("cancelled")
self.snapshot.alpha = 0.0
cell.hidden = false
UIView.animateWithDuration(0.10, animations: { () -> Void in
self.snapshot.center = cell.center
self.snapshot.transform = CGAffineTransformIdentity
self.snapshot.alpha = 0.0
//undo fade out
cell.alpha = 1.0
}, completion: { (finished) in
self.snapshot.removeFromSuperview()
})
}
case UIGestureRecognizerState.Ended:
//clean up
print("ended")
let cell: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
cell.hidden = false
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.snapshot.center = cell.center
self.snapshot.transform = CGAffineTransformIdentity
self.snapshot.alpha = 0.0
//undo fade out
cell.alpha = 1.0
}, completion: { (finished) in
self.snapshot.removeFromSuperview()
})
break
default:
break
}
}else{
gesture.cancelsTouchesInView = true
}
}
func customSnapshotFromView(inputView: UIView) -> UIView {
// Make an image from the input view.
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0)
inputView.layer.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
// Create an image view.
let snapshot = UIImageView(image: image)
snapshot.layer.masksToBounds = false
snapshot.layer.cornerRadius = 0.0
snapshot.layer.shadowOffset = CGSize(width: -5.0, height: 0.0)
snapshot.layer.shadowRadius = 5.0
snapshot.layer.shadowOpacity = 0.4
return snapshot
}
}
When I scroll after dragging it looks like:
As you can see cell is not appearing again. I want to drag and drop static cell and I want to save it's position so I will not rearrange again when I scroll.
Sample project for more Info.
This is just a demo project But I have added many elements into my cell and every cell have different UI.
There is a library that does exactly what you are looking to do with a very similar approach. It's called FMMoveTableView but it's for cells with a datasource.
I think that what is causing your problem is that when you move the cells around and then you scroll the datasource from the storyboard is no longer in sync with the table and therefore your cell object can't be redrawn.
I think you should implement your table this way:
Make your 4 cells custom cells.
Subclass each one.
Create an Array with numbers 1 to 4
Reorder the array on long drag
Override cellForRowAtIndexPath to show the right cell for the right number
You can drag uitableview cell from uitableview delegates .......
1) set the table view editing style to none in its delegate.
2) implement table view delegate to enable dragging of cell i.e canMoveRowAtIndexPath methods...
You can create multiple dynamic cells.
You'll just have to dequeue cells with correct identifier.
Are you doing this for layout purposes only, maybe a UICollectionView or a custom made UIScrollView could do the job?
Never the less, I have a solution:
Create a IBOutlet collection holding all your static UITableViewCells
Create a index list to simulate a "data source"
Override the cellForRowAtIndexPath to draw using your own index list
When updating the list order, update the indexList so that the view "remembers" this change
This Table view controller explains it all:
class TableViewController: UITableViewController {
#IBOutlet var outletCells: [UITableViewCell]!
var indexList = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
// Prepare a index list.
// We will move positions in this list instead
// of trying to move the view's postions.
for (index, _) in outletCells.enumerate() {
indexList.append(index)
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Use dynamic count, not needed I guess but
// feels better this way.
return outletCells.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Use the index path to get the true index and return
// the view on that index in our IBOutlet collection
let realIndexForPos = indexList[indexPath.row]
return outletCells[realIndexForPos]
}
#IBAction func onTap(sender: AnyObject) {
// Simulating your drag n drop stuff here... :)
let swapThis = 1
let swapThat = 2
tableView.moveRowAtIndexPath(NSIndexPath(forItem: swapThis, inSection: 0), toIndexPath: NSIndexPath(forItem: swapThat, inSection: 0))
// Update the indexList as this is the "data source"
// Do no use moveRowAtIndexPath as this is never triggred
// This one line works: swap(&indexList[swapThis], &indexList[swapThat])
// But bellow is easier to understand
let tmpVal = indexList[swapThis]
indexList[swapThis] = indexList[swapThat]
indexList[swapThat] = tmpVal
}
}
To create the IBOutlet use the Interface Builder.
Use the Referencing Outlet Collection on each Table View Cell and drag, for each, to the same #IBOutlet in your controller code.

Resources