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.
Related
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 written sample code here - when i tap button "Select" it should select all the cells of UICollectionView. But is does not work and no error is reported. Below is code
// collectionView is name of UICollectionView
let collectonViewCount = collectionView.numberOfItemsInSection(0);
for i in 0...collectonViewCount {
let indexPath = NSIndexPath(forRow: i, inSection: 0)
collectionView.selectItemAtIndexPath(indexPath, animated: true, scrollPosition: .None)
}
I'm not sure what could be wrong in the above code.
cybergeeeek,
you can try
for i in 0...collectonViewCount {
let indexPath = NSIndexPath(forRow: i, inSection: 0)
collectionView.delegate?.collectionView!(collectionView, didSelectItemAtIndexPath: indexPath)
}
This answer ofcourse assumes that you have set the collectionView delegate and have implemented didSelectItemAtIndexPath this will select the collectionView cell and will trigger didSelectItemAtIndexPath :)
I have a PFQueryTableViewController in which I am trying to learn some tableView interactions.
What I currently have: The tableView cell increases its height on a tap didSelectRowAtIndexPath or I can basically segue cell related data to next viewController using performSegueWithIdentifier together with prepareForSegue using a single tap.
In the below code, if I comment the "Tap code to increases height of tableView cell" code, I can detect double tap. Other wise the single tap increases the height as the code is inside didSelect block.
What I need:
On Single tap, cell height increases or decrease. While the cell height is increased, if I double tap, it should segue cell data to next UIViewController.
If not, the single tap should increase or decrease the height. And double tap should segue cell data to next UIViewController
Code is as below:
var selectedCellIndexPath: NSIndexPath?
var SelectedCellHeight = CGFloat() // currently set to 480.0 tableView height
var UnselectedCellHeight = CGFloat() // currently set to 300.0 tableView unselected hight
....
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if let selectedCellIndexPath = selectedCellIndexPath {
if selectedCellIndexPath == indexPath {
return SelectedCellHeight
}
}
return UnselectedCellHeight
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as! DataViewTableViewCell!
if cell == nil {
cell = DataViewTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
}
// Below is the double Tap gesture recognizer code in which
// The single tap is working, double tap is quite difficult to get
// as the cell height increases on single tap of the cell
let doubleTap = UITapGestureRecognizer()
doubleTap.numberOfTapsRequired = 2
doubleTap.numberOfTouchesRequired = 1
tableView.addGestureRecognizer(doubleTap)
let singleTap = UITapGestureRecognizer()
singleTap.numberOfTapsRequired = 1
singleTap.numberOfTouchesRequired = 1
singleTap.requireGestureRecognizerToFail(doubleTap)
tableView.addGestureRecognizer(singleTap)
// Tap code to increases height of tableView cell
if let selectedCellIndexPath = selectedCellIndexPath {
if selectedCellIndexPath == indexPath {
self.selectedCellIndexPath = nil
cell.postComment.fadeIn()
cell.postCreatorPicture.alpha = 1
cell.postDate.fadeIn()
// cell.postTime.fadeIn()
cell.postCreator.fadeIn()
} else {
self.selectedCellIndexPath = indexPath
}
} else {
selectedCellIndexPath = indexPath
}
tableView.beginUpdates()
tableView.endUpdates()
}
func doubleTap() {
print("Double Tap")
}
func singleTap() {
print("Single Tap")
}
The other way I can make it work is, if I can get NSIndexPath of selected cell outside of didSelectRowAtIndexPath code, I can basically use the double tap to perform if else to get the required action. Can you get NSIndexPath outside the tableView default blocks?
Got it working!
First: Define var customNSIndexPath = NSIndexPath() and add the below code inside the didSelectRowAtIndexPath code block.
customNSIndexPath = indexPath
print(customNSIndexPath)
Second: Remove "Tap code to increases height of tableView cell" code from didSelectRowAtIndexPath and add to the singleTap() function. And perform segue using the doubleTap().
func doubleTap() {
print("Double Tap")
performSegueWithIdentifier("MainToPost", sender: self)
}
func singleTap() {
print("Single Tap")
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as! DataViewTableViewCell!
if cell == nil {
cell = DataViewTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
}
if let selectedCellIndexPath = selectedCellIndexPath {
if selectedCellIndexPath == customNSIndexPath {
self.selectedCellIndexPath = nil
} else {
self.selectedCellIndexPath = customNSIndexPath
}
} else {
selectedCellIndexPath = customNSIndexPath
}
tableView.beginUpdates()
tableView.endUpdates()
}
This is how you screw up working late without sleeping. It was the easiest thing to do. Thanks
Note: If someone has better solution or this one is wrong in some xyz case, please do comment. If yours is better it would really help others.
var lastClick: TimeInterval = 0.0
var lastIndexPath: IndexPath? = nil
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let now: TimeInterval = Date().timeIntervalSince1970
if (now - lastClick < 0.3) && (lastIndexPath?.row == indexPath.row ) {
print("Double Tap!")
} else {
print("Single Tap!")
}
lastClick = now
lastIndexPath = indexPath
}
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.
I have a custom class (superclass UITableViewController).
I have an add button that:
Begins table updates.
Inserts row in table.
Ends table updates.
Calls selectRowAtIndexPath.
Calls didSelectRowAtIndexPath.
Unfortunately, didDeselectRowAtIndexPath is not being called after programatically selecting a row.
Note: Using Swift (not Objective-C).
#IBAction func addButton(sender: UIBarButtonItem) {
let indexPathZero: NSIndexPath = NSIndexPath(forRow: 0, inSection: 0)
todoController.addNewTodoItem(nameOfItem: "test5")
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([indexPathZero], withRowAnimation: UITableViewRowAnimation.Top)
self.tableView.contentOffset.y = 64
self.tableView.endUpdates()
// Ask user for input
editCell(indexPathZero)
}
func editCell(indexPath: NSIndexPath) {
self.tableView.selectRowAtIndexPath(indexPath, animated: true, scrollPosition: UITableViewScrollPosition.None)
self.tableView(self.tableView, didSelectRowAtIndexPath: indexPath)
}
Solution
let indexPathZero: NSIndexPath = NSIndexPath(forRow: 0, inSection: 0)
todoController.addNewTodoItem(nameOfItem: "test5")
let currSelectedIndexPath = self.tableView.indexPathForSelectedRow()
if currSelectedIndexPath != nil {
self.tableView.deselectRowAtIndexPath(currSelectedIndexPath!, animated: true)
self.tableView(self.tableView, didDeselectRowAtIndexPath: currSelectedIndexPath!)
}
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([indexPathZero], withRowAnimation: UITableViewRowAnimation.Top)
self.tableView.contentOffset.y = 64
self.tableView.endUpdates()
// Ask user for input
editCell(indexPathZero)
See the apple docs. When you call selectRowAtIndexPath, it will not call didSelectRowAtIndexPath.
"Calling this method does not cause the delegate to receive a tableView:willSelectRowAtIndexPath: or tableView:didSelectRowAtIndexPath: message, nor does it send UITableViewSelectionDidChangeNotification notifications to observers."