I have a table view with two custom cells, one of those cells has custom button (subclassed uibutton to hold the indexpath). I created a custom delegate for the cell with the custom button, here is the code of the delegate method:
func indexOfClassDeletedInTableView(path:NSIndexPath, table:UITableView) {
let classToDelete = self.classesScheduled[path.row]
ManagedObjectsController.sharedInstance.deleteScheduledClass(classToDelete) { (succedeed) in
if (succedeed == true) {
NSNotificationCenter.defaultCenter().postNotificationName(self.itemSuccesFullyDeletedFromPersonalView, object: nil)
self.classesScheduled.removeAtIndex(path.row)
table.deleteRowsAtIndexPaths([path], withRowAnimation: .Fade)
} else {
print("failed to delete class from personal schedule")
}
}
}
the problem is that the first time the method gets called, the table deletes the cell at the indexpath given and the one below it. After the first call it works fine and suddenly the extra cell deleted reappears. The custom delegate method (indexOfClassDeletedInTableView) is in my datasource. Thank you in advance.
Related
Current Situation:
I have a UITableView with custom cells. In the cells are 1 label and 1 textview.
Scrolling is enabled for the UITableView.
Below the Table, I have a button to save the entries from the textview.
My Problem is:
I get only the value from the first row.
From the other rows, I get always an empty text, but only if the user has scrolled.
I don't understand why and I hope you can help me.
Here is my Code:
#objc func save()
{
for i in 0..<labels.count
{
let indexPath = IndexPath(item: i, serction: 0)
let cell = self.SearchTable.dequeueReusableCell(withIdentifier: "searchCell",
for: indexPath) as! SearchCell
print("Index: \(indexPath)")
if(cell.EditField.text = ""
{
continue;
}else{
...
}
}
}
Debugger
First Row: FirstRow
Other Rows: SecondRow
Cells in a tableview are reused, so a cell at an index path that is not visible could have values from a completely different cell. If you want to get all the cells that are currently visible on the screen, you could use tableView.visibleCells docs to get all the cell that are currently on screen and extract data from there.
Alternatively, you could choose to not implement cell reuse and make your table view static. You can do this in Interface Builder, or you could also choose to create all the cells up front and return your pre-made cells in tableView(_:cellForRowAt:). Note that a setup like this is okay for small datasets, but has terrible performance for larger sets so be aware that this might not be the best way to do things. It really depends on your situation. This method of doing things would end up looking a bit like this:
var cells = [UITableViewCell]()
override func viewDidLoad() {
// do all kinds of stuff
for field in fields { // or whatever else mechanism you use as your datasource
let cell = UITableViewCell()
// configure your cell
cells.append(cell)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return cells[indexPath.row]
}
The third and last way you might want to solve this is to add a delegate to your cells and set the view controller as the delegate. If the text changes, call the delegate with the cell's index path and the new text value. You can then store this text somewhere in the view controller and read it from there when you save rather than pulling it from the cell's textfield. Personally I would prefer this method.
Rather than iterate over the cells in the tableview, you could just get the data from the data source.
Whenever the text in the cell's text field changes you could update the data source and then use the information from there to perform your save.
You must be having some kind of data source anyway, otherwise what happens to the text when the cell scrolls off the screen and comes back on again? If you aren't storing the text somewhere then you've got nothing to populate the cell with in the table views cellForRow(... delegate method.
I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.
Hi in my application i want to add particular view in cell.contentview now situation is that where i can add this code. reason is if i am puting this code on cellforitematindexpath it will be added in last cell(indxpath.row) reason is in cellforrowatindexpath last cell will be loaded last so view will be added in last cell. while i want it on that cell which was displaying at current time
Here is my code. simply have to add subview
let cell : pdfImageCell = collectionView.dequeueReusableCell(withReuseIdentifier: "pdfImageCell", for: indexPath) as! pdfImageCell
cell.contentView.addSubview(userResizableView1)
Get the visible cell index like below in UICollectionView. You can also get multiple cells visible if they are visible on screen so selected the cell you want.
var visibleCurrentCell: IndexPath? {
for cell in self.collectionView.visibleCells {
let indexPath = self.collectionView.indexPath(for: cell)
return indexPath
}
return nil
}
Then get the cell from your index and add the subview in that cell.
You can add UIView in awakeFromNib() method. It will get the call only once.
class pdfImageCell : UICollectionViewCell {
//you can add here
override func awakeFromNib() {
var userResizableView1 = UIView()
self.contentView.addSubview(userResizableView1)
}
}
- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths
Here it is a method to reload the specific indexPaths in your collectionView
you can update your datasource on click and your cellForItemAtIndex will know from your data source that it has to have the subview this time
if (datasourcearray[indexpPath.row].isSubViewToBeAdded)
{
self.contentview.addsubview(subViewToBeAdded)
}
I have a custom table view cell with two buttons. If either one is pressed, I want the table view to delete the row. I already know the code for that:
myArray.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
I just would like to call this from my custom cell. I have it set up like so:
#IBAction func handleDecline(sender: AnyObject)
{
// Remove table view row
}
#IBAction func handleApprove(sender: AnyObject)
{
// Remove table view row
}
How would I go about doing that?
You should not handle deletion of a cell from the cell itself. It violates MVC pattern. Events should be delegated to the view controller and handled there with deleteRows method like this:
tableView.deleteRows(at: [indexPath], with: .fade)
Also tableView's dataSource must be updated accordingly, otherwise app will crash.
Here you can find an example of how you can delegate an event from cell to view controller
I have read through similar questions but I haven't been able to solve it. I have a tableview which has a custom cell with a button to add and remove a class. I created a custom delegate to save and remove the class, and to change the state of the button. Changing the state of the button works fine, but when I'm scrolling the buttons don't hold the state (I'm guessing is because I'm dequeuing the cells)
in my cell for row at index path i tried checking to change the state of the button:
let isInSchedule = self.isClassScheduled(classAttend, from: self.classesInSchedule!)
if isInSchedule == true {
cell.addRemoveButton.selected = true
} else {
cell.addRemoveButton.selected = false
}
and here is my delegate method where I save or remove the class
func indexOfClassSelectedWithButton(index: NSIndexPath, tableView:UITableView, and button: AddRemoveClass) {
if let currentlySavedClasses = ManagedObjectsController.sharedInstance.getAllScheduledClasses() as? [ClassScheduled] {
let classSelected = self.classes[index.section]
switch button.selected {
case true:
for classItem in currentlySavedClasses {
if classSelected.presentation?.title == classItem.presentation?.valueForKey("title") as? String {
ManagedObjectsController.sharedInstance.deleteScheduledClass(classItem)
button.selected = false
}
}
break
default:
if let classSelectedBreakout = classSelected.breakout?.valueForKey("breakoutID") as? String {
let canSave = self.isBreakoutAvailable(classSelectedBreakout, allClasses: currentlySavedClasses)
if canSave {
ManagedObjectsController.sharedInstance.createScheduledClass(from: classSelected)
button.selected = true
} else {
NSNotificationCenter.defaultCenter().postNotificationName(timeConlictNotication, object: nil)
}
}
break
}
}
}
it changes the button state but when I start scrolling up or down the buttons don't hold the state (I'm aware is probably because I'm revising my cell so it is taking any cell that it is available. Any help is greatly appreciated.
Also, I have my cell within my view controller in story board. Why is is that if i do cell = PresentationCell() all of the views in my cell are nil? Im just trying to not reuse the cell as my last solution.
Reusable table view cells do not keep the state so that to keep the button hold it's states you have to do an another check based on your condition when the cell will appear on the screen.
My suggestion is implement
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if isInSchedule == true {
cell.addRemoveButton.selected = true
} else {
cell.addRemoveButton.selected = false
}
}
Another thing is remembering reset states of cell before using by implement "prepareForReuse" in your custom cell.
override func prepareForReuse() {
}
I hope this would be help.
You should be doing two things
Override "prepareForReuse" in your UITableViewCell subclass to reset
the tableview cell to some default state
in tableView:cellForRowAtIndexPath: will dequeu a re-usable cell.
Since that cell is being re-used and may have already been
associated with a different indexPath you have to re-configure it.
In your case, you would set the state of the button to reflect the
indexPath you are about to assign the cell to
The solution came from #Michael " any state you need must be maintained outside of the cell" since the tableView reuses the cells that are already created I had to add a property to the class Im populating the tableview with (I used a bool but it can be anything) and based on that property I set the state of the button in the method cellForRowAtIndexPath.