After press button I want to update my cell height so atm I'm used this closure and call him in my button method, but is not working ):
my viewDidLoad():
tableView.estimatedRowHeight = 238.0
tableView.rowHeight = 238.0
my closure which try to change cell height after press a button
cellClosureHeight = { [weak self] in
self?.tableView.beginUpdates()
self?.tableView.estimatedRowHeight = 118.0
self?.tableView.rowHeight = 118.0
self?.tableView.endUpdates()
}
my cell init
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch productCells[indexPath.row] {
case .popCorn(let viewModel):
let cell = tableView.dequeueReusableCell(
withIdentifier: TableViewCells.popCornOrder.identifier,
for: indexPath
) as! ProductTableViewCell
cell.set(viewModel: viewModel)
return cell
case .barWater(let viewModel):
let cell = tableView.dequeueReusableCell(
withIdentifier: TableViewCells.barWaterOrder.identifier,
for: indexPath
) as! ProductTableViewCell
cell.set(viewModel: viewModel)
return cell
}
}
Without animate
tableView.reloadData()
animate
tableView.beginUpdates()
let indexPaths = [IndexPath.init(row: 0, section: 0)] //the cells you want to update
tableView.reloadRows(at: indexPaths, with: .automatic)
tableView.endUpdates()
tableView.reloadData()
after making changes in table you should have to reload the table to reflect the new changes that you have applied.
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.
I never get this print . it is like is always nil, doesnt matter how much i scroll it up or down :/
if let update = tableView.cellForRow(at: indexPath ) as? RestCell {
print("VISIBLE CELL")
}
Complete code..
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : RestCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! RestCell
// Pass data to Cell :) clean the mess at the View Controller ;)
cell.restData = items[indexPath.row]
// Send cell so it can check update the image to the right cell ;)
// cell.cell = tableView.cellForRow(at: indexPath ) as? RestCell
//print("LA CELDA ES \(tableView.cellForRow(at: indexPath ))")
if let update = tableView.cellForRow(at: indexPath ) as? RestCell {
print("VISIBLE CELL")
}
return cell
}
You have not returned your cell from cellForRow method so this is the reason why your if statement is returning false. I'm not sure why you are trying to do this but there have to be a better way. If you want to look for visible cells you can use UITableView visibleCells variable.
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)
}
Good Morning
I created a custom TableViewCell, which internally has a CollectionViewCell and a directional arrow. I was able to make the cell expand and collapse however I would like it when I expand one cell and I click on another one the previous collapse. Example: If cell is expanded and cell is tapped, I want cell to contract and cell to expand at the same time. So that only one cell can be in its expanded state in any given moment changing the targeting arrow.
Example:
If cellA is expanded and cellB is tapped, I want cellA to contract and cellB to expand at the same time. So that only one cell can be in its expanded state in any given moment changing the targetting arrow.
My code:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch(selectedIndexPath)
{
case nil:
let cell = myTableView.cellForRow(at: indexPath) as! TableViewCell
cell.arrowImagem.image = UIImage(named:"content_arrow2")
selectedIndexPath = indexPath
default:
if selectedIndexPath! == indexPath
{
let cell = myTableView.cellForRow(at: indexPath) as! TableViewCell
cell.arrowImagem.image = UIImage(named:"content_arrow")
//cell.reloadInputViews()
selectedIndexPath = nil
}
}
self.myTableView.beginUpdates()
self.myTableView.reloadData()
//myTableView.reloadRows(at: [indexPath], with: .automatic)
self.myTableView.endUpdates()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let index = indexPath
if selectedIndexPath != nil{
if(index == selectedIndexPath)
{
return 147
}
else{
return 67
}
}
else
{
return 67
}
}
You can do that but you need to do some coding. First of all detect when a row is selected (keep the Index Path of the selected row in a variable). When a row is selected you will add the index path of the new selected row and that of the previously selected (that you have in the variable) in an array that you pass to the tableView reloadRows from inside didSelectRowAt, this function will force redrawing of the two rows and the heightForRowAt function will be called to get the height of both rows, in heightForRowAt you will check the currently selected row (do you remember the variable I mentioned earlier?) and return the expanded height for that, while for the others you return the collapsed height. You should get the result you want. Hope that will help.
You can implement most of the functionality in the didSet handler:
var selectedIndexPath: IndexPath? {
didSet {
guard indexPath != oldValue else {
return
}
if let oldValue = oldValue {
let cell = myTableView.cellForRow(at: indexPath) as? TableViewCell
cell?.arrowImagem.image = UIImage(named:"content_arrow")
}
if let indexPath = indexPath {
let cell = myTableView.cellForRow(at: indexPath) as? TableViewCell
cell?.arrowImagem.image = UIImage(named:"content_arrow2")
}
let rowsToUpdate = [indexPath, oldValue].compactMap { $0 }
self.myTableView.beginUpdates()
// I don't think you actually need to reload rows here but keeping your code here...
self.myTableView.reloadRows(at: rowsToUpdate, with: .automatic)
self.myTableView.endUpdates()
}
}
and then in your didSelect:
if indexPath == selectedIndexPath {
selectedIndexPath = nil
} else {
selectedIndexPath = indexPath
}
(written without testing)
Here's a problem which I have been stuck at for quite some time now.
Here's the code
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({() -> Void in
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
self.collectionViewLove?.reloadData()}, completion: nil)}
I have a button inside each UICollectionViewCell which deletes it on clicking. The only way for me to retrieve the indexPath is through the button tag. I have initialized the button tag in
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
However every time I delete, the first time it deletes the corresponding cell whereas the next time it deletes the cell follwing the one I clicked. The reason is that my button tag is not getting updated when I call the function reloadData().
Ideally, when I call the reloadData() ,
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
should get called and update the button tag for each cell. But that is not happening. Solution anyone?
EDIT:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: UIImage(named: "preloader"))
let item = self.wishlist?.results[indexPath.row]
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.text = item?.title
cell.itemName.numberOfLines = 1
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.price.adjustsFontSizeToFitWidth = true
cell.deleteButton.tag = indexPath.row
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = indexPath.row
cell.buyButton.backgroundColor = UIColor.blackColor()
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
A couple of things:
You're doing too much work in cellForItemAtIndexPath--you really want that to be as speedy as possible. For example, you only need to register the nib once for the collectionView--viewDidLoad() is a good place for that. Also, you should set initial state of the cell in the cell's prepareForReuse() method, and then only use cellForItemAtIndexPath to update with the custom state from the item.
You shouldn't reload the data until the deletion is complete. Move reloadData into your completion block so the delete method is complete and the view has had time to update its indexes.
However, it would be better if you didn't have to call reloadData in the first place. Your implementation ties the button's tag to an indexPath, but these mutate at different times. What about tying the button's tag to, say, the wishlist item ID. Then you can look up the appropriate indexPath based on the ID.
Revised code would look something like this (untested and not syntax-checked):
// In LoveListCollectionViewCell
override func prepareForReuse() {
// You could also set these in the cell's initializer if they're not going to change
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.numberOfLines = 1
cell.price.adjustsFontSizeToFitWidth = true
cell.buyButton.backgroundColor = UIColor.blackColor()
}
// In your UICollectionView class
// Cache placeholder image since it doesn't change
private let placeholderImage = UIImage(named: "preloader")
override func viewDidLoad() {
super.viewDidLoad()
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: placeholderImage)
let item = self.wishlist?.results[indexPath.row]
cell.itemName.text = item?.title
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.deleteButton.tag = item?.id
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = item?.id
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
func removeFromLoveList(sender: AnyObject?) {
let id = sender.tag
let index = wishlist?.results.indexOf { $0.id == id }
let indexPath = NSIndexPath(forRow: index, inSection: 0)
collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
wishlist?.results.removeAtIndex(index)
}
It's probably not a good idea to be storing data in the cell unless it is needed to display the cell. Instead your could rely on the UICollectionView to give you the correct indexPath then use that for the deleting from your data source and updating the collectionview.
To do this use a delegate pattern with cells.
1.Define a protocol that your controller/datasource should conform to.
protocol DeleteButtonProtocol {
func deleteButtonTappedFromCell(cell: UICollectionViewCell) -> Void
}
2.Add a delegate property to your custom cell which would call back to the controller on the delete action. The important thing is to pass the cell in to that call as self.
class CustomCell: UICollectionViewCell {
var deleteButtonDelegate: DeleteButtonProtocol!
// Other cell configuration
func buttonTapped(sender: UIButton){
self.deleteButtonDelegate.deleteButtonTappedFromCell(self)
}
}
3.Then back in the controller implement the protocol function to handle the delete action. Here you could get the indexPath for the item from the collectionView which could be used to delete the data and remove the cell from the collectionView.
class CollectionViewController: UICollectionViewController, DeleteButtonProtocol {
// Other CollectionView Stuff
func deleteButtonTappedFromCell(cell: UICollectionViewCell) {
let deleteIndexPath = self.collectionView!.indexPathForCell(cell)!
self.wishList.removeAtIndex(deleteIndexPath.row)
self.collectionView?.performBatchUpdates({ () -> Void in
self.collectionView?.deleteItemsAtIndexPaths([deleteIndexPath])
}, completion: nil)
}
}
4.Make sure you set the delegate for the cell when configuring it so the delegate calls back to somewhere.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//Other cell configuring here
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("identifier", forIndexPath: indexPath)
(cell as! CustomCell).deleteButtonDelegate = self
return cell
}
}
I was facing the similar issue and I found the answer by just reloading collection view in the completion block.
Just update your code like.
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
}, completion: {
self.collectionViewLove?.reloadData()
})
which is mentioned in UICollectionView Performing Updates using performBatchUpdates by Nik