I have a button in my custom cell that deletes the cell.
So i have a delegate that removes it.
code in view controller:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("swipeTableViewCell", forIndexPath: indexPath) as! swipeTableViewCell
cell.initCell(self, indexPath: indexPath, text: data[indexPath.row])
return cell
}
delegate method:
func removeCell(indexPath: NSIndexPath){
data.removeAtIndex(indexPath.row)
table.beginUpdates()
table.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
table.endUpdates()
}
code in cell:
func initCell(handler: handleCells, indexPath: NSIndexPath, text: String) {
self.handler = handler
self.indexPath = indexPath
}
button pressed:
#IBAction func OnDelButtonClickListener(sender: UIButton) {
self.handler.removeCell(indexPath)
}
This removes the cell with animation but the reloadData is not called and then the cells have the wrong indexPath.
So when I press a second cells delete the wrong cell gets removed.
If I call reloadData after table.endUpdates() there is no animation.
if I call
let indexSet = NSIndexSet(index: indexPath.section)
self.table.reloadSections(indexSet, withRowAnimation: UITableViewRowAnimation.Automatic)
instead of
table.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
I don't get a removal animation.
Any suggestions?
Thanks
Have a look at Apple's programming guide for UITableViews, at the row deleting section.
I may be missing something in your code, but it looks like you don't actually delete the object in the datasource that corresponds to your deleted cell. Try removing the object from your datasource in the removeCell function before you delete the row.
func removeCell(indexPath: NSIndexPath){
// here you delete the object form the datasource
// after that, you do this
table.beginUpdates()
table.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
table.endUpdates()
}
I think the key problem is in the Cell indexPath could not update when the table view delete the cell.
so we can try create a help array in the ViewController,help us update the really data to delate.
lazy var listHelper:Array<Int> = {
var array = [Int]()
for i in 0...self.data.count {
array.append(i)
}
return array
}()
update the removeCell function to this :
func removeCell(indexPath: NSIndexPath) {
// if first delete delete the date, and remove index in help list
if indexPath.row < listHelper.count - 1 && indexPath.row == listHelper[indexPath.row] {
data.removeAtIndex(indexPath.row)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
listHelper.removeAtIndex(indexPath.row)
}else {
// if indexPath.row != listHelper[indexPath.row],we find the really data we want to delete, used Array extension .indexOf
let locationData = listHelper.indexOf(indexPath.row)
data.removeAtIndex(locationData!)
// we create NSIndexPath and delete it.
let theindexPath = NSIndexPath(forRow: locationData!, inSection: 0)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([theindexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
listHelper.removeAtIndex(locationData!)
}
}
the Extension of Array :
extension Array {
func indexOf <U: Equatable> (item: U) -> Int? {
if item is Element {
return Swift.find(unsafeBitCast(self, [U].self), item)
}
return nil
}
}
My English is poor. and U can see the code . I had try it, and it can work. I hope can solve you problem.
Related
I have a problem with my cells where I remove/insert to my tableView, I delete and insert my cells like that :
self.tableView.beginUpdates()
user.lobbySurvey.remove(at: 0)
self.tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .fade)
self.tableView.endUpdates()
Insert:
self.tableView.beginUpdates()
user.lobbySurvey.insert(surveyEnded, at: rowToInsert)
self.tableView.insertRows(at: [IndexPath(row: rowToInsert, section: 0)], with: .fade)
self.tableView.endUpdates()
lobbySurvey is the data array of the TableView.
Problem is the cell I add keep the design of the first cell that I delete previously.
I think this issue is because I check if the card is draw or not. If not checked: the view of the card is added each time the cell is reuse.
this is how I check if the cell is draw or not :
func drawSurveyEnded(){
if(cardIsDraw == false){
surveyEnded.draw(cardView: self.cardView)
surveyEnded.delegate = self
self.addCardShadow()
cardIsDraw = true
}
}
The function is called many times cause to the reuse system of iOS. So I think my problem come from here.
Why my cell in my tableView keep the design of the previous cell that I deleted?
Other code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:"Celll") as! CardCell
for survey in user.lobbySurvey{
let index = user.lobbySurvey.index(where: {
//get the current index is nedeed else the cells reuse lazy
$0 === survey
})
if indexPath.row == index{
var surveyState : UserSurvey.state
surveyState = survey.state
switch surveyState{
case .selectSurvey:
cell.drawCard(statutOfCard: .selectSurvey)
case .goSurvey:
cell.drawCard(statutOfCard: .goSurvey(picture: survey.picture))
case .surveyEnded:
cell.drawCard(statutOfCard: .surveyEnded(picture: survey.picture))
case .surveyWork:
print("survey in progress to vote")
case .surveyWaiting:
cell.drawCard(statutOfCard: .surveyWaiting(selfSurveyId: survey.id, timeLeft: survey.timeLeft, picture: survey.picture))
case .buyStack:
cell.drawCard(statutOfCard: .buyStack(supView : self.view))
}
}
}
cell.delegate = self
cell.delegateCard = self
cell.layer.backgroundColor = UIColor.clear.cgColor
cell.backgroundColor = .clear
tableView.backgroundColor = .clear
tableView.layer.backgroundColor = UIColor.clear.cgColor
return cell
}
Draw card is a simple switch of the state of the survey where called the good function to create the card like this one I posted before : drawSurveyEnded
In your CardCell you have to implement prepareForReuse and clean the cell's view from there. So when cells are recycled, they are cleaned from the previous views that were drawn.
You should implement:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Update the cell here
}
This way you can make sure the cell gets updated whenever it is ready to be shown.
Remember the UIKit framework keeps a cache for the cells, so it can just reuse them.
In my custom cell I have a timer. When the count down reach 0, I call my delegate method and the cell is automatically deleted.
The problem is that when the second cell reach 0, my app crashes with the error fatal error: Index out of range.
In my custom cell I setup my data:
protocol MyDelegateName {
func removeOfferExpired(offerId: String, indexPath: IndexPath)
}
class MyCustomCell: UITableViewCell {
var offer:Offers?
var cellIndexPath:IndexPath?
var delegate:MyDelegateName?
func setupData(offer:Offers, indexPath:IndexPath){
self.offer = offer
self.cellIndexPath = indexPath
//...other code not relevant
}
//When the time reach zero I call the following method
func updateTime() {
if timeLeft > 0 {
timeLeft = endTime.timeIntervalSinceNow
offerExpiresLabel.textColor = UIColor.white
offerExpiresLabel.text = timeLeft.hmmss
}else {
offerExpiresLabel.textColor = UIColor.red
offerExpiresLabel.text = "Offer Expired"
timer.invalidate()
self.delegate?.removeOfferExpired(offerId: (self.offer?.offer_id!)!, indexPath: self.cellIndexPath!)
}
}
In my ViewController I setup my cell data inside cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let offer = offers[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! MyCustomCell
cell.setupData(offer: offer, indexPath: indexPath)
cell.delegate = self
return cell
}
Then inside func removeOfferExpired(offerId: String, indexPath: IndexPath) I have tried to use:
1. self.offers.remove(at: indexPath.row)
self.tableView.reloadData()
2. self.offers.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
self.tableView.reloadData()
3. //and even try to "wrap" it inside begin/end updates
tableView.beginUpdates()
self.offers.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
it always crashes the second times. I understand that the indexPath I assign to the cell in setupData is not the same after the first cell is deleted but I thought reloadData was the way to go to update the indexPath in the remaining cells.
Your primary issue is that fact that you tell a cell its index path and your cell then passes that index path to its delegate. But a cell's index path isn't stable. It changes as other rows are added, removed, or moved.
The method of your cell protocol should pass itself (the cell) as a parameter, not an index path. Then the delegate can query the table view to find the cell's up-to-date index path and perform the row deletion based on that up-to-date index path.
As rmaddy said, what I was doing it was completely wrong. This is what I did based on his answer:
func updateTime() {
if timeLeft > 0 {
timeLeft = endTime.timeIntervalSinceNow
offerExpiresLabel.textColor = UIColor.white
offerExpiresLabel.text = timeLeft.hmmss
}else {
offerExpiresLabel.textColor = UIColor.red
offerExpiresLabel.text = "Offer Expired"
timer.invalidate()
// when the time reach zero I passed self to the delegate instead of the indexPath
self.delegate?.removeOfferExpired(offerId: (self.offer?.offer_id!)!, cell: self as UITableViewCell)
}
}
protocol MyDelegateName {
func removeOfferExpired(offerId: String, cell: UITableViewCell) // delegate method now passes the cell instead of the index
}
func removeOfferExpired(offerId: String, cell: UITableViewCell) {
// and then I get the index path from the cell
let indexPath = tableView.indexPath(for: cell)
self.offers.remove(at: (indexPath?.row)!)
self.tableView.deleteRows(at: [indexPath!], with: .automatic)
}
I have implemented the delete functionality for any row based on the index passed.
Each cell has a button to initiate delete for that row. I take the cell.tag to detect the row and pass to delete function which uses indexPath and deleteRowAtIndexPaths(...).
Now, the problem happens when I keep on deleting the 0th row. Initially, it deletes correctly. 0th row is gone. 1st row replaces the 0th row.
Now, if I delete 0th row again, it deletes the current 1st row.
The reason I understood is that cell.tag is not updated.
What exactly an I doing wrong ?
The problem is not consistent. If I wait between the deletes, it is ok. If I delete one row after another. It keeps on deleting some other row.
How should I proceed now ? I have searched for this already and unable to find proper solution or guide ?
Here are the main pieces of code
// Typical code having Programmatic UITableView
// ...
func addTestEvent(cell: MyCell) {
func onSomeAction() {
dispatch_async(dispatch_get_main_queue(), {
self.removeRow(cell.tag)
})
}
...
// onSomeAction() called on click on the button
}
func test(cell: MyCell) -> () {
...
addTestEvent(cell)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier( NSStringFromClass(MyCell), forIndexPath: indexPath) as! MyCell
cell.tag = indexPath.row
cell.test = { (cell) in self.test(cell) }
return cell
}
func removeRow(row: Int) {
let indexPath = NSIndexPath(forItem: row, inSection: 0)
tableView.beginUpdates()
posts.removeAtIndex(row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.endUpdates()
}
The key point is not to use cell.tag to identify the cell. Rather use the cell directly. Thanks Vadian for the comment. It is not a good practice to keep indexPath in cell tag. Now I know why !
This answer gave me the major hint to resolve the problem.
https://stackoverflow.com/a/29920564/2369867
// Modified pieces of code. Rest of the code remain the same.
func addTestEvent(cell: MyCell) {
func onSomeAction() {
dispatch_async(dispatch_get_main_queue(), {
self.removeRow(cell)
})
}
// ...
// onSomeAction() called on click on the button
}
func removeRow(cell: UITableViewCell) {
let indexPath = tableView.indexPathForRowAtPoint(cell.center)!
let rowIndex = indexPath.row
// ...
}
Add tableView.reloadData() after deleting a cell. That worked for me.
I have a delegate function that changes a UITableViewCell's section location on tap:
//On cell tap, expand
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 0
{
let data = firstDataSource[indexPath.row]
tableView.beginUpdates()
secondDataSource.append(data)
firstDataSource.removeAtIndex(indexPath.row)
let newIndexPath = NSIndexPath(forRow: find(secondDataSource, data)!, inSection: 1)
tableView.moveRowAtIndexPath(indexPath, toIndexPath: newIndexPath)
tableView.endUpdates()
} else if indexPath.section == 1 {
let data = secondDataSource[indexPath.row]
tableView.beginUpdates()
firstDataSource.append(data)
secondDataSource.removeAtIndex(indexPath.row)
let newIndexPath = NSIndexPath(forRow: find(firstDataSource, data)!, inSection: 0)
tableView.moveRowAtIndexPath(indexPath, toIndexPath: newIndexPath)
tableView.endUpdates()
}
}
I would like for this action to occur when I fire an IBAction from a button tap, but I'm not sure how I can access the indexPath argument as is given in the delegate function. Here is my current code for the IBAction button:
#IBAction func checkOffTask(sender: UIButton) {
var checkedOff = false
let indexOfCell = sender.tag
sender.setImage(UIImage(named: "checkbox-checked"), forState: UIControlState.Normal)
self.tableView.beginUpdates()
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forItem: indexOfCell, inSection: 1)], withRowAnimation: UITableViewRowAnimation.Automatic)
self.tableView.endUpdates()
}
Any idea on how to get the delegate function to work for the IBAction function?
Okay, so your action is coming from a button, that button is within a table view cell, the question is: what's the indexPath of that table view cell.
The easiest way is:
get the location of the button within the table view;
ask for the index path at that position.
E.g.
#IBAction func checkOffTask(sender: UIButton!) {
let originInTableView = self.tableView.convertPoint(CGPointZero, fromView: sender)
let indexPath = self.tableView.indexPathForRowAtPoint(originInTableView)
}
Okay, this is a workaround I have used before.
Doesn't look pretty but does the job:
UIButton *likeButton = (UIButton *)sender;
UITableViewCell *cell = (UITableViewCell *)likeButton.superview.superview;
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
Just find the correct superview for the button which is of UICollectionView Class type.
P.S. - My Swift is not the best. Hoping you can convert this.
I have a Image view in my cell which displays a checkmark icon.
What I want to do is when you touch a cell the checkmark should appear. I got this working, but my problem now is that I can't remove the checkmark from the previous selected cell. - only one cell should be able to be selected.
I've tried to get this working in didSelectRowAtIndexPath but I can't get it right so I am stuck right now.
Update:
var selectedRow: NSIndexPath?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: searchCityTableViewCell!
if (indexPath.section == 0) {
cell = tableView.dequeueReusableCellWithIdentifier("staticCityCell") as! searchCityTableViewCell
cell.titleLabel?.text = "Within \(stateName)"
}else if (indexPath.section == 1) {
cell = tableView.dequeueReusableCellWithIdentifier("cityCell") as! searchCityTableViewCell
let state = cities[indexPath.row]
cell.configureWithStates(state)
if indexPath == selectedRow {
cell.cityImage.select()
} else {
cell.cityImage.deselect()
}
}
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
let paths:[NSIndexPath]
if let previous = selectedRow {
paths = [indexPath, previous]
} else {
paths = [indexPath]
}
selectedRow = indexPath
tableView.reloadRowsAtIndexPaths(paths, withRowAnimation: .None)
}
Keep track of which row is currently selected. Add a property to your ViewController:
var selectedRow: NSIndexPath?
In didSelectRowAtIndexPath:
let paths:[NSIndexPath]
if let previous = selectedRow {
paths = [indexPath, previous]
} else {
paths = [indexPath]
}
selectedRow = indexPath
tableView.reloadRowsAtIndexPaths(paths, withRowAnimation: .None)
In cellForRowAtIndexPath:
if indexPath == selectedRow {
// set checkmark image
} else {
// set no image
}
An important thing to note is that the state of which row is selected should be stored in the model and not in the cell. The table should reflect the state of the model. In this case, the model can simply be the indexPath of the selected row. Once the model is updated (selectedRow is set), the affected rows should reload their state.