I am using xCode 7 beta and Swift to implement a tableview with MGSwipeTableCells. I am doing this because I need to have a swipe button on both the left and right of each cell. Both of these buttons needs to remove the cell from the tableview.
I tried doing this by using the convenience callback method when adding the buttons to the cells:
// Layout table view cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("newsFeedCell", forIndexPath: indexPath) as! NewsFeedCell
cell.layoutIfNeeded()
// Add a remove button to the cell
let removeButton = MGSwipeButton(title: "Remove", backgroundColor: color.removeButtonColor, callback: {
(sender: MGSwipeTableCell!) -> Bool in
// FIXME: UPDATE model
self.numberOfEvents--
self.tableView.deleteSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
return true
})
cell.leftButtons = [removeButton]
However, once I delete the first cell, all the indices are thrown off and the callback now deletes an incorrect cell. That is, if I delete cell_0, cell_1 now becomes the first 0th in the table. However, the callback for the buttons associated with cell_1 delete the cell with index 1 even though it is actually now the 0th cell in the table.
I tried to implement the MGSwipeTableCell delegate methods, but to no avail. None of these methods were ever called in the execution of my code. How should I fix this problem? Will implementing the delegate solve this issue? If, so would it be possible to provide an example? If not, can you please suggest an alternate way to have tableview cells with swipe buttons on both sides that can delete said cells?
You can also do something like this to get the correct indexPath:
let removeButton = MGSwipeButton(title: "Remove", backgroundColor: color.removeButtonColor, callback: {
(sender: MGSwipeTableCell!) -> Bool in
let indexPath = self.tableView.indexPathForCell(sender)
self.tableView.deleteSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
return true
})
Using the delegate methods will allow for cleaner button creation and table cell removal, because the buttons will only be created for the cell when it is swiped (saves memory) and you can capture a weak reference to the 'sender' (an MGTableViewCell, or custom type) in the handler, from which you can then get the index path. Follow their example on Github:
MGSwipeTableCell/demo/MailAppDemo/MailAppDemo/MailViewController.m
Then in your cellForRowAtIndexPath, be sure to set the cell's delegate to 'self.' It looks like you're missing this, and it should fix your problem with delegate methods.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let reuseIdentifier = "cell"
let cell = self.table.dequeueReusableCellWithIdentifier(reuseIdentifier) as! MGSwipeTableCell!
cell.delegate = self
// Configure the cell
return cell
}
Happy coding!
Related
I'm working through an exercise which uses tableviews. I noticed within a test during the exercise, they use a method I haven't needed in the past when implementing tableviews from storyboards. The method is:
func register(AnyClass?, forCellReuseIdentifier: String)
After reading the short description of this function in the reference pages. I'm curious to know what does apple mean by term "registers"? I half assume that since we are doing this exercise programmatically at the moment, this function is only needed if you're creating UITableviews programmatically. If this statement is incorrect, please let me know as I'd like to learn more.
Here is the code from the example:
func test_CellForRow_DequesCellFromTableView(){
let mockTableView = MockTableView()
mockTableView.dataSource = sut
mockTableView.delegate = sut
mockTableView.register(ItemCell.self, forCellReuseIdentifier: "ItemCell")
sut?.itemManger?.add(ToDoItem.init(title: "Foo"))
mockTableView.reloadData()
_ = mockTableView.cellForRow(at: IndexPath.init(row: 0, section: 0))
XCTAssertTrue(mockTableView.cellGotDequeed)
}
The DequeueReusable methods are there to check if any reusable cells are left before creating new ones. Hope you have an idea about the working of reusable cells
What happens when the queue is empty? Now we do need to create a cell. We can follow 2 methods to create a cell,
Create cell manually
Create it automatically by registering cell with a valid xib file
METHOD 1
if you do it with manually, you must check cell is empty or not after dequeueReusableCell check. Just like below,
// create a cell for each table view row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Reuse an old cell if exist else return nil
let cell:UITableViewCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell!
//check cell is nil if nil you want to allocate it with proper cell
if(cell == nil){
//create cell manually
cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "CellSubtitle")
}
// do stuff to the cell here
return cell
}
METHOD 2
We could create the cell manually like above which is totally fine. But it would be convenient if the table view would create the cell for us directly.
That way we don't have to load it from a nib or instantiate it.
For registering a cell with a xib or class we use func register(AnyClass?, forCellReuseIdentifier: String) method. Let see an example,
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(MyCell.self, forCellReuseIdentifier: "Cell")
}
// ...
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath) as MyCell
// no "if" - the cell is guaranteed to exist
// ... do stuff to the cell here ...
cell.textLabel.text = // ... whatever
// ...
return cell
}
You are "registering" your custom Cell class - ItemCell - for reuse as a cell for your tableview.
See: https://developer.apple.com/reference/uikit/uitableview/1614888-register
"Register" tells XCode that the cell exists. A cell is registered under a "reuse identifier." This is a unique string that corresponds to your TableViewCell, in this case ItemCell.
A cell can also be registered in the Storyboard by filling out the "Identifier" in the cell's attributes inspector.
I am trying to get cells' string using a button in a custom UITableViewCell.But when I tap the button app crashes due to this error :
fatal error: unexpectedly found nil while unwrapping an Optional value .
I tried print something and it works!.but when I try to get other cells' string app crashes. Here is my code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cellID:String!
var cell = OrderCell()
switch indexPath.section {
.
.
.
.
.
case 5 :
if indexPath.row == 0 {cellID = "Submit" }
cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! OrderCell
cell.submitButton.addTarget(self, action: #selector(OrderViewController.submitNewOrder), for: .touchUpInside)
default:
break
}
return cell
}
func submitNewOrder() {
//Title
let index1:IndexPath = IndexPath(row: 0, section: 0)
let cell1: OrderCell = tableView.cellForRow(at: index1) as! OrderCell
print("order is \(cell1.orderTitle.text!)")
}
I am sure that row and section are right!. Also I tired same method with #IBAction and it works fine ! What is the problem ?
Thank you
All that you need is,
self.tableView(tableView, cellForRowAt: index1)
Rather than
tableView.cellForRow(at: index1) as! OrderCell
How it works ??
In tableview, cell gets reused. Your code will crash only if you scroll your tableview and later tap on button in some cell at bottom, reason cell at index path (0,0) is reused so tableView will not keep the cell at index path (0,0) hence when you call self.tableView(tableView, cellForRowAt: index1) it returns nil.
On the other hand what you should have probably done is to ask the data source delegates to return the cell rather than the tableView itself. In this case, your own VC is data source, hence I wrote
self.tableView(tableView, cellForRowAt: index1)
This will get the reference to cell at index path (0,0) even if its not there with tableView and hand it over to u rather than nil :)
Suggestion :
Though the above code works absolutely fine, I personally recommend having IBAction from button to cell, As cell holds the button, it makes sense logically to create a IBAction of button in cell rather than in tableViewController.
You can always declare a delegate in your cell and implement it in your TableViewController to inform the button tap and letting know which button was tapped.
This way code will be clean to anybody who will look at it even once. Cell is responsible for handling IBAction of its subview and informs only tableViewController what to do on tapping button. TableViewController on the other hand just performs the task and never worry about which button on which it is tapped.
EDIT:
let index1:IndexPath = IndexPath(row: 0, section: 0)
let cell1 = self.tableView(tableView, cellForRowAt: index1) as! OrderCell
I dint change anything you can use your existing code as shown above :) Simply replace cellForRowAt :)
EDIT 2 :
Though the above provided code works absolutely fine, In a chat that we had (You can refer the chat in comment section) I realised that the cell at indexPath (0,0) has a textFiled.
Above solution though works fine for label, on using it with TextField it returned the textField.text as "".
This was obviously because of mistake in cellForRowAtIndexPath and because of cell reuse strategy of UITableView.
Though I have suggested formal and more proper suggestion in suggestion section of answer above, I believe because the answer is accepted, its my duty to provide fully functional code which will work with textField as well :)
Simplest thing to do would be update the data source once user enters some text in the textField of cell at indexPth (0,0) and update the textField text properly in cellForRowAt index path of tableView.
Doing it properly will no longer return textField text as "" on scrolling down or on cell getting reused.
Here is how I did it :)
Step 1:
Make your cell a textField delegate and implement textFieldDidEndEditing :)
class MyTableViewCell: UITableViewCell,UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
//will see implementation later
}
}
Step 2 :
Declare a protocol in your custom cell to inform the tableViewController to update its data source with user text :)
protocol updateDataSource {
func updateDataSource(with userText : String)
}
Step 3 :
Create a delegate in cell :)
var delegate : updateDataSource? = nil
Step 4 :
Implement textFieldDidEndEditing,
func textFieldDidEndEditing(_ textField: UITextField) {
self.delegate?.updateDataSource(with: textField.text!)
}
Step 5 :
Confirm delegate in your ViewController (TableViewController)
extension ViewController : updateDataSource {
func updateDataSource(with userText: String) {
//update your data source
//I dont have any hence am updating a instance variable in my VC
self.userEnterredText = userText
}
}
Finally update your CellForRowAtIndexPath as
cell.delegate = self
if indexPath.row == 0 && self.userEnterredText != nil {
cell.testTextField.text = self.userEnterredText!
}
else {
cell.testTextField.text = ""
}
return cell
This will make sure that when you call self.tableView(tableView, cellForRowAt: index1) on a data source for a cell which is reused, to call cell for row at index path, and because of your code in cellForRowAt now your textfield will be populated properly :)
Now you can still use your code
let index1:IndexPath = IndexPath(row: 0, section: 0)
let cell1: OrderCell = tableView.cellForRow(at: index1) as! OrderCell
print("order is \(cell1.orderTitle.text!)")
With textField as well :)
Hope it helps :)
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 downloaded a collection of images and loaded them into a collectionView. I want to be able to add the individual item selected to an array I declared globally whenever I press on an individual cell so then I can loop through them to be deleted from core data later. This line prints fine in terms of the order of the item - print("You selected cell #(indexPath.item)!"), but the 2nd time I press another cell to add to the array, I get an fatal error: Index out of range error. I don't know what I'm getting this.
var selectedCell: [Int] = [] -> Declared globally
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// handle tap events
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
print("You selected cell #\(indexPath.item)!")
if self.selectedCell.contains(indexPath.item){
print("Item already added")
} else {
self.selectedCell.append(indexPath.item)
}
if selectedCell.count > 0 {
toolbarButton.title = "Remove Item"
}
// let selectCell:UICollectionViewCell = collectionView.cellForItemAtIndexPath(indexPath)!
// selectCell.contentView.backgroundColor = UIColor.whiteColor()
}
iOS 9.3, Xcode 7.3
I honestly think that "fatal error: Index out of range" does not apply to the simpler array of integer indexes that you declared, I think that it is associated to the index of the collection view itself.
Looks like you are conforming to the various protocols of UICollectionView, namely UICollectionViewDataSource, UICollectionViewDelegate, because you are at least receiving callbacks to the cell selected method.
First thing that jumps at me is...
Where is title property for toolbarButton declared, is it a custom subclass of UIButton, because title is not a set able property of standard UIButton class. This is how you'd typically set the title of a UIBUtton...
self.toolbarButton.setTitle("Remove Item", forState: .Normal)
Another thing is,
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
The variable cell is never used in that function, it should give you a warning.
Also, check all of your uses of the array variable selectedCell, in the if statement where you check the count value you are not using self.selectedCell for example. Not sure what this would do, but I think this would cause your array to get re-synthesized, so count will be reset each time.
There are few other items that I don't understand, here is the code that I would use. Please check your code and syntax.
The following code works:
var selectedCell: [Int] = []
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
// handle tap events
print("You selected cell #\(indexPath.item)!")
if (self.selectedCell.contains(indexPath.item))
{
print("Item already added")
}
else
{
self.selectedCell.append(indexPath.item)
}
print("selectedCell.count: \(self.selectedCell.count)")
if (self.selectedCell.count > 0)
{
self.toolbarButton.setTitle("Remove Item", forState: .Normal)
print("selectedCell: \(self.selectedCell)")
}
else
{
//nil
}
}
The output would look something like this:
Note: When you click the same cell twice it is not added (in this case 8), once you have at least 1 item in the array, then the button title changes.
If you still can not figure it out, then see this post, it explains how to implement a collection view very well: https://stackoverflow.com/a/31735229/4018041
Hope this helps! Cheers.
Issue 1: Check Marks Keep Disappearing when scrolling.
Issue 2: Need help adding/removing from array with unique ID to prevent duplicates.
I am trying to insert/remove a cellTextLabel from an empty array. I can't seem to find a good solution. Here's what I've tried and why it failed.
Bad Option 1
// Error = Out of range (I understand why)
myArray.insert(cell.textLabel.text, atIndex: indexPath)
Bad Option 2
// I won't have a way to reference the array item afterwards when I need to remove it. Also, this option allows for the same string to be entered into the array multiple times, which is not good for me.
myArray.insert(cell.textLabel.text, atIndex: 0)
Below is the code so far, any help is greatly appreciated. Thank you.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = indexPath.row
let cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier("items", forIndexPath: indexPath) as UITableViewCell
var myRowKey = myArray[row]
cell.textLabel.text = myRowKey
cell.accessoryType = UITableViewCellAccessoryType.None
cell.selectionStyle = UITableViewCellSelectionStyle.None
return cell
}
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
let selectedCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell!
if selectedCell.accessoryType == UITableViewCellAccessoryType.None {
selectedCell.accessoryType = UITableViewCellAccessoryType.Checkmark
}
var selectedItem = selectedCell.textLabel.text!
println(selectedItem)
}
func tableView(tableView: UITableView!, didDeselectRowAtIndexPath indexPath: NSIndexPath!) {
let deSelectedCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell!
if deSelectedCell.accessoryType == UITableViewCellAccessoryType.Checkmark {
deSelectedCell.accessoryType = UITableViewCellAccessoryType.None
}
var deSelectedItem = deSelectedCell.textLabel.text!
println(deSelectedItem)
}
Issue 1: Your checkmarks keep disappearing when you're scrolling because of the following line in the didDeselectRowAtIndexPath method:
let deSelectedCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell!
The call to cellForRowAtIndexPath will create a NEW cell. It will NOT modify the currently visible cell on the screen in your UITableView. This is because cells are reused as items scroll on and off the screen, with new data loaded into them.
To retain the selection status of your cells, you will need to upgrade your data model a bit. Right now your data comes from the myArray which is a String array. You could try something as follows instead:
struct Item {
var name: String // The string value you are putting in your cell
var isSelected: Bool // The selected status of the cell
}
Then you would define your data something like this:
var myArray = [
Item(name: "Cell 1 value", isSelected: false),
Item(name: "Cell 2 value", isSelected: false),
...
]
And your tableView:didSelectRowAtIndexPath method would look more like this:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Toggle the selected state of this cell (e.g. if it was selected, it will be deselected)
items[indexPath.row].isSelected = !items[indexPath.row].isSelected
// Tell the table to reload the cells that have changed
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
tableView.endUpdates()
// Reloading that cell calls tableView:numberOfRowsInSection and refreshes that row within the tableView using the altered data model (myArray array)
}
Next time you tap that row, the tableView:didSelectRowAtIndexPath method will fire again and toggle the selected state of that cell. Tell that cell to reload will refresh the cell that is actually visible on the screen.
Issue 2: Without knowing too much about the type of data you want to keep unique and how you are adding/removing in ways that could add duplicates, you might want to take a look at this answer for removing duplicate elements from your array. One way is to use a set, which will not preserve order but will ensure elements only occur once.