How to fix UITableView cell repeat in swift 4 - ios

When I create a new cell it will automatically mark
or when I marked the first one, I was creating multiple
Cell, there will be duplicates marked
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let itemCellIdentifier = "itemCell"
guard let itemCell = itemCardTableView.dequeueReusableCell(withIdentifier: itemCellIdentifier, for: indexPath) as? ItemCardTableViewCell else {
return UITableViewCell()
}
let itemCard = dataManager.items[indexPath.row]
itemCell.itemTitle.text = itemCard.title
if itemCard.isFinish {
itemCell.itemCellView.backgroundColor = UIColor(red: 0, green: 123, blue: 0)
}
return itemCell
}
Add cell method
let confirm = UIAlertAction(title: "η’Ίθͺ", style: .default) { (action: UIAlertAction) in
guard let title = addAlert.textFields?.first?.text else { return }
let newItem = ItemCard(title: title, isFinish: false)
self.dataManager.items.append(newItem)
let indexPath = IndexPath(row: self.itemCardTableView.numberOfRows(inSection: 0), section: 0)
self.itemCardTableView.insertRows(at: [indexPath], with: .left)
When I create new data isFinish = false
How can I fix data duplication?

You should provide else case to set default background color.
if itemCard.isFinish {
itemCell.itemCellView.backgroundColor = UIColor(red: 0, green: 123, blue: 0)
} else {
itemCell.itemCellView.backgroundColor = UIColor.white
}

If I understood your question right, You should hold the isFinish flag somewhere out of the cell, because it's being reused by tableView.
You can create the finished = [Bool]() is your UIViewController and every time check finished[indexPath.row] to see if it's marked or not and pass the boolean to your cell.

Related

Select cell from a saved indexPath

I am trying to create a tableview that selects the cell using an indexPath.
I have saved the indexPath of selected cell & save it to my data array for each question. However when I reload the tableView how do I get the indexPath to change that particular cell's background view to how it looked when I select the previous button.
var dataArray: [MultipleChoice] = [
MultipleChoice(
question: "Question 1",
options: ["Answer 1", "Answer 2", "Answer 3", "Answer 4"],
rightAnswer: "Answer 1",
subject: "General",
idxPath: [0,0]),
MultipleChoice(
question: "Question 2",
options: ["Answer 1", "Answer 2", "Answer 3", "Answer 4"],
rightAnswer: "Answer 2",
subject: "General",
idxPath: [0,0]),
#IBAction func nextButtonPressed(_ sender: Any) {
if var index = dataBrain.questionNumber {
if index + 1 < dataBrain.dataArray.count {
index += 1
dataBrain.questionNumber = index
scoreLabel.text = "\(dataBrain.questionNumber! + 1)/\(dataBrain.dataArray.count)"
answerHidden = true
table.reloadData()
}
}
#IBAction func previousButtonPressed(_ sender: Any) {
if var index = dataBrain.questionNumber {
if index - 1 >= 0 {
index -= 1
dataBrain.questionNumber = index
scoreLabel.text = "\(dataBrain.questionNumber! + 1)/\(dataBrain.dataArray.count)"
table.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: AnswerTableViewCell.identifier) as! AnswerTableViewCell
//Load Options:
if let index = dataBrain.questionNumber {
let data = dataBrain.dataArray[index]
cell.answerLabel.text = data.options![indexPath.row - 1]
cell.selectionStyle = .none
}
//Load Right/Wrong Options
let index = dataBrain.questionNumber
let data = dataBrain.dataArray[index!]
let rightAnswer = data.rightAnswer!
if cell.answerLabel.text == rightAnswer {
cell.confirmButton.setImage(UIImage(systemName: "checkmark.circle"), for: .normal)
cell.confirmButton.imageView?.tintColor = UIColor.green
} else {
cell.confirmButton.setImage(UIImage(systemName: "xmark.circle"), for: .normal)
cell.confirmButton.imageView?.tintColor = UIColor.red
}
//Load Background Color/Text
cell.delegate = self
cell.selectedBackgroundView = .none
cell.confirmButton.isHidden = answerHidden
let selectedCell = dataBrain.dataArray[index!].idxPath
if selectedCell == [0,0] {
cell.answerView.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
cell.answerLabel.textColor = #colorLiteral(red: 0.4228360057, green: 0.4478931427, blue: 0.4731111526, alpha: 1)
} else {
table.selectRow(at: selectedCell, animated: false, scrollPosition: .none)
}
return cell
When you implement tableView(_ tableView: UITableView, cellForRowAt ..., your job is to return a cell for the given indexPath -- do not change the table. Your source of truth is dataBrain.dataArray -- get the information, put it in a cell, and return it. That's it.
So, in this code:
let selectedCell = dataBrain.dataArray[index!].idxPath
if selectedCell == [0,0] {
cell.answerView.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
cell.answerLabel.textColor = #colorLiteral(red: 0.4228360057, green: 0.478931427, blue: 0.4731111526, alpha: 1)
} else {
table.selectRow(at: selectedCell, animated: false, scrollPosition: .none)
}
The if should be more like this:
if selectedCell[0] == indexPath.section, selectedCell[1] == indexPath.row {
// The cell I am making is selected -- set it up
} else {
// The cell I am making is not selected, set it up also
// (it could be a reused cell that is colored for selection, so ALWAYS set the properties.
}
Do not call table.selectRow
Now, when you want a cell to change, just reload it. Either reloadData (not ideal, but you can do it to make sure things work), or the other reload methods that target specific changes.

Swiping on a tableview is deselecting all selected rows trailingSwipeActionsConfigurationForRowAt

Recently implemented trailingSwipeActionsConfigurationForRowAt , where after swiping from right to left showing two options and its working fine. But the problem is when i select multiple rows or single row, after swiping the row/s they are getting deselected. Is there a way to keep the selection even after swiping?
Below is my code
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let renameAction = contextualToggleRenameAction(forRowAtIndexPath: indexPath)
let lastResAction = contextualToggleLastResponseAction(forRowAtIndexPath: indexPath)
let swipeConfig = UISwipeActionsConfiguration(actions: [renameAction, lastResAction])
swipeConfig.performsFirstActionWithFullSwipe = false
return swipeConfig
}
func contextualToggleLastResponseAction(forRowAtIndexPath indexPath: IndexPath) -> UIContextualAction {
let sensorData = sensorsList?[indexPath.row]
var lastResponse = ""
if sensorData != nil{
if let lstRes = sensorData!["last_response"] as? String{
lastResponse = lstRes
}
}
let action = UIContextualAction(style: .normal, title: lastResponse) { (contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in
print("Last Response Action")
}
action.backgroundColor = UIColor(red: 61/255, green: 108/255, blue: 169/255, alpha: 1.0)
return action
}
Holy crap, I fixed this stupid issue.
Yes, yes make sure tableView.allowsSelectionDuringEditing = true and tableView.allowsMultipleSelectionDuringEditing = true.
But...
Shout-out to this SO answer which almost directly led me on the way to success (see here).
KEY: Re-select/de-select the rows in your implementation of setEditing, which is a method you override. UITableView goes into editing mode when swiping.
IMPORTANT NOTE: Call super.setEditing(...) before any of your code, as shown below, or it likely won't work, or at least not perfectly.
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
for i in 0..<your data.count {
if this item is selected {
self.tableView.selectRow(at: IndexPath(row: i, section: 0), animated: false, scrollPosition: .none)
} else {
self.tableView.deselectRow(at: IndexPath(row: i, section: 0), animated: false)
}
}
}
A swipe is considered as an edit, so you can enable allowsSelectionDuringEditing if you want to keep the selected state:
tableView.allowsSelectionDuringEditing = true
Depending on indexPathsForSelectedRows doesn't always give the expected result.
Instead you should maintain and array of selectedIndexPaths.
Here is a code snippet to demonstrate:
var selectedIndexPaths = [IndexPath]()
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if selectedIndexPaths.contains(indexPath) {
selectedIndexPaths.removeAll { (ip) -> Bool in
return ip == indexPath
}else{
selectedIndexPaths.append(indexPath)
}
}
You can simply do this :
Create array in your controller for selected index
var arrSelectedIndex : [Int] = []
In didSelect,
if arrSelectedIndex.contains(indexPath.row) { // Check index is selected or not
// If index selected, remove index from array
let aIndex = arrSelectedIndex.firstIndex(of: indexPath.row)
arrSelectedIndex.remove(at: aIndex!)
}
else {
// If index not selected, add index to array
arrSelectedIndex.append(indexPath.row)
}
// reload selected row, or reloadData()
self.tblView.reloadRows([indexPath.row], with: .automatic)
Edit
In trailingSwipeActionsConfigurationForRowAt,
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
if self.arrSelectedIndex.contains(indexPath.row) {
let action = UIContextualAction(style: .normal, title: "") { (action, view, handler) in
}
action.backgroundColor = .green
let configuration = UISwipeActionsConfiguration(actions: [])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
let action = UIContextualAction(style: .normal, title: "Selected") { (action, view, handler) in
}
action.backgroundColor = .green
let configuration = UISwipeActionsConfiguration(actions: [action])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
Output

UICollectionView | Cell reusable

SO, UICollectionView is being a real pain for me right now. Consider I have a UIViewController which has a UICollectionView embedded in it. Well each cell of the CollectionView is almost the entire width of the UIViewController. And each cell contains some buttons and images. When I select one button and tend to make the button retain its state, the CollectionView reuses the cell and kind of duplicates the cell states across other cells as well. However when I try to put the cells in an array and kind of want to check the states of cells in that array, the cellForItemAt method overwrites those cells. I am so confused. Please help. Even prepareForReuse in UICollectionViewCell isn't helping. Here is some code:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AddressCollectionViewCell
cell.shopAddressDetailLbl.text = ""
cell.addressObj = addresses[indexPath.row]
cell.configureCellForAddress(cell.addressObj)
cell.cellTag = indexPath.row
cell.cellDelegate = self
if addressCells.contains(cell) == false {
addressCells.append(cell)
} else {
if cell.isAddressConfirmed == true {
cell.confirmAddress.setTitle("CONFIRMED", for: .normal)
cell.confirmAddress.isEnabled = false
cell.confirmAddress.backgroundColor
= UIColor(red: 0, green: 100/255, blue: 0, alpha: 1)
addressCells[indexPath.row] = cell
}
}
return cell
}
extension AddressesCollectionViewController: AddressCollectionViewCellDelegate {
func confirmBtnPressed(confirmAddressObj: Address, cell:AddressCollectionViewCell) {
for cellTemp in addressCells {
if cellTemp == cell && cellTemp.isAddressConfirmed == false {
if let dele = addressCollectionViewDelegate {
cellTemp.isAddressConfirmed = true
dele.configureCellsAccordingToChanges(cell: cellTemp)
}
}
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
cellTag = 0
confirmAddress.setTitle("Confirm Address", for: .normal)
confirmAddress.backgroundColor = APP_UNIVERSAL_COLOR
confirmAddress.isEnabled = true
}
Any help is more than appreciated.
πŸ™Œ #Vadian, #Abu Ul Hassan πŸ‘
Pretty slick! To others who need help in this regard. Vadian suggested in comments that I just need to update and monitor my model and thats exactly what I did. SO here it goes:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AddressCollectionViewCell
cell.shopAddressDetailLbl.text = ""
cell.addressObj = addresses[indexPath.row]
cell.configureCellForAddress(cell.addressObj)
cell.cellTag = indexPath.row
cell.cellDelegate = self
if addresses[indexPath.row].isConfirmed! == true {
cell.confirmAddress.setTitle("CONFIRMED", for: .normal)
cell.confirmAddress.isEnabled = false
cell.confirmAddress.backgroundColor = UIColor(red: 0, green: 100/255, blue: 0, alpha: 1)
} else {
cell.confirmAddress.setTitle("Confirm Address", for: .normal)
cell.confirmAddress.isEnabled = true
cell.confirmAddress.backgroundColor = APP_UNIVERSAL_COLOR
}
return cell
}
extension AddressesCollectionViewController: AddressCollectionViewCellDelegate {
func confirmBtnPressed(confirmAddressObj: Address, cell:AddressCollectionViewCell) {
if confirmAddressObj.isConfirmed! == false {
if let dele = addressCollectionViewDelegate {
cell.isAddressConfirmed = true
dele.configureCellsAccordingToChanges(cell: cell)
}
}
}
}
And its ALIVE :D

Three entities hierarchical but in the third the first isn't accounted

I have an app with three TableViews and each is in a separate ViewController. I want pass the data from the firstVC to the secondVC to the thirdVC. Club -> Member -> transaction. If there is just one club, there is no problem.
In the second club, there are the members from the second club, which has to be like this.
Problem
But if the user clicks on the member, there are the transactions from the members of the first club at the indexPath from the first club.
My thoughts
So I have to account the club as well to the transactions. But my solutions just threw errors.
So maybe you have a good solution. I found a solution here but this didin't helped me.
Here is my TableView code:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return member?.transactions?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableViewTransaction.dequeueReusableCell(withIdentifier: "transactionCell", for: indexPath)
if let transaction = member?.transactions?[indexPath.row] {
cell.textLabel?.text = transaction.reason
let moneystring = String(transaction.money)
cell.detailTextLabel?.text = moneystring + " Fr."
if transaction.money < 0.0 {
cell.detailTextLabel?.textColor = UIColor.red
}
if transaction.money > 0.0 {
cell.detailTextLabel?.textColor = UIColor(red: 0, green: 110/255, blue: 0, alpha: 0.8)
}
}
return cell
}
Club fetch:
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate
else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchrequest: NSFetchRequest <Verein> = Verein.fetchRequest()
do {
clubs = try managedContext.fetch(fetchrequest)
tableViewClub.reloadData()
} catch {
print("Could not fetch")
Member fetch:
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchrequest: NSFetchRequest<Member> = Member.fetchRequest()
do {
members = try managedContext.fetch(fetchrequest)
self.tableViewMember.reloadData()
} catch {
print("Could not fetch")
}
Description of the App-
UITableView cells are reused, so if you load a previously used cell -- with its label's already set -- could be used for a new one. Because you're only setting the cell's labels if a transaction is found, if no transaction is found the cell will simply show the data that was previously loaded. You need to do something like this.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableViewTransaction.dequeueReusableCell(withIdentifier: "transactionCell", for: indexPath)
// clear the cell
cell.textLabel?.text = "" // blank or whatever the default should be
cell.detailTextLabel?.text = ""
// load any new data
if let transaction = member?.transactions?[indexPath.row] {
cell.textLabel?.text = transaction.reason
let moneystring = String(transaction.money)
cell.detailTextLabel?.text = moneystring + " Fr."
if transaction.money < 0.0 {
cell.detailTextLabel?.textColor = UIColor.red
}
if transaction.money > 0.0 {
cell.detailTextLabel?.textColor = UIColor(red: 0, green: 110/255, blue: 0, alpha: 0.8)
}
}
return cell
}

Select ALL TableView Rows Programmatically Using selectRowAtIndexPath

I'm trying to programmatically select all rows in my tableview using the following code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:myTableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell") as! myTableViewCell
cell.accessoryType = .None
if allJobsSelected {
let bgColorView = UIView()
bgColorView.backgroundColor = UIColor(red: 250/255, green: 182/255, blue: 17/255, alpha: 1)
cell.contentView.backgroundColor = UIColor(red: 250/255, green: 182/255, blue: 17/255, alpha: 1)
cell.selectedBackgroundView = bgColorView
cell.accessoryType = .Checkmark
cell.highlighted = false
cell.selected = true
// cell.accessoryType = .Checkmark
self.tableView.selectRowAtIndexPath(indexPath, animated: true, scrollPosition: UITableViewScrollPosition.None)
self.tableView(self.tableView, didSelectRowAtIndexPath: indexPath)
}
var job: Jobs!
job = jobs[UInt(indexPath.row)] as! Jobs
cell.reports2JobTitle.text = job.jobTitle
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.tableView.allowsMultipleSelection = true
if let cell:myTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? myTableViewCell {
let bgColorView = UIView()
bgColorView.backgroundColor = UIColor(red: 250/255, green: 182/255, blue: 17/255, alpha: 1)
cell.contentView.backgroundColor = UIColor(red: 250/255, green: 182/255, blue: 17/255, alpha: 1)
cell.selectedBackgroundView = bgColorView
cell.accessoryType = .Checkmark
cell.highlighted = false
self.tableView.selectRowAtIndexPath(indexPath, animated: true, scrollPosition: UITableViewScrollPosition.Bottom)
}
}
My issue is that only the rows that have been dequeued are added to my table's data model when I segue to the next viewcontroller. In order to add all the rows to my table's data model I have to manually scroll through the whole table. How can I change this so all the selected rows are added to my table's data model without having to scroll through the whole table?
What I cannot understand is that after all my rows are selected I then loop through my indexPaths as follows but not all of the indexPaths are added unless I first scroll through the entire table.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "reportsDisplay") {
let controller = segue.destinationViewController as! ReportsDisplayViewController
var selectedJob : Jobs!
if let indexPaths = tableView.indexPathsForSelectedRows {
for i in 0 ..< indexPaths.count {
let thisPath = indexPaths[i]
selectedJob = jobs[UInt(thisPath.row)] as! Jobs
let jobTitle = selectedJob.jobTitle
let id = selectedJob.identifier
jobsToReport.append(jobTitle)
jobsID.append(id)
}
}
controller.reportedJobs = jobsToReport
controller.idOfJobs = jobsID
}
}
For Swift 3 and answering your question literally, regardless of your code.
func selectAllRows() {
for section in 0..<tableView.numberOfSections {
for row in 0..<tableView.numberOfRows(inSection: section) {
tableView.selectRow(at: IndexPath(row: row, section: section), animated: false, scrollPosition: .none)
}
}
}
If you want to inform the tableview delegate, use this method:
func selectAllRows() {
for section in 0..<tableView.numberOfSections {
for row in 0..<tableView.numberOfRows(inSection: section) {
let indexPath = IndexPath(row: row, section: section)
_ = tableView.delegate?.tableView?(tableView, willSelectRowAt: indexPath)
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
tableView.delegate?.tableView?(tableView, didSelectRowAt: indexPath)
}
}
}
At the time allJobsSelected becomes true, you need to call the UITableView method selectRowAtIndexPath(_:animated:scrollPosition:) for each row of your table. In my case, I attached this functionality to the right bar button item which I named Select All. Calling this from cellForRowAtIndexPath is surely not the right place.
#IBAction func doSelectAll(sender: UIBarButtonItem) {
let totalRows = tableView.numberOfRowsInSection(0)
for row in 0..<totalRows {
tableView.selectRowAtIndexPath(NSIndexPath(forRow: row, inSection: 0), animated: false, scrollPosition: UITableViewScrollPosition.None)
}
}
Functional solution (Swift 5.1)
extension UITableViewDataSource where Self: UITableView {
/**
* Returns all IndexPath's in a table
* ## Examples:
* table.indexPaths.forEach {
* selectRow(at: $0, animated: true, scrollPosition: .none) // selects all cells
* }
*/
public var indexPaths: [IndexPath] {
return (0..<self.numberOfSections).indices.map { (sectionIndex: Int) -> [IndexPath] in
(0..<self.numberOfRows(inSection: sectionIndex)).indices.compactMap { (rowIndex: Int) -> IndexPath? in
IndexPath(row: rowIndex, section: sectionIndex)
}
}.flatMap { $0 }
}
}
Solution for Swift 5.5
This method can be logically made an extension of UITableView and must be called from the main thread:
extension UITableView {
// Must be called from the main thread
func selectAllRows() {
let totalRows = self.numberOfRows(inSection: 0)
for row in 0..<totalRows {
self.selectRow(at: IndexPath(row: row, section: 0), animated: true, scrollPosition: .none)
}
}
}
Note: this code is only for the only section

Resources