I tried to copy only the necessary code to show my problem. I have a tableview with dynamic content. I created a prototype cell and it has a user name and 10 stars (it's a rating page). People in the group are allowed to rate other people. Everything is working ok, but I have a problem when I scroll down. If I rate my first user with 8 stars, when I scroll down then some user that was in the bottom area of the tableview, appears with the rate that I gave to my first user. I know that tableview reuse cells. I tried many things but with no success. Hope someone can help me on that.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let model = users[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("RatingCell") as! RatingTableViewCell
cell.tag = indexPath.row
cell.playerLabel.text = model.name
cell.averageView.layer.borderWidth = 1
cell.averageView.layer.borderColor = Color.Gray1.CGColor
cell.averageView.layer.cornerRadius = 5
cell.starsView.userInteractionEnabled = true
cell.averageLabel.text = "\(user.grade)"
for i in 0...9 {
let star = cell.starsView.subviews[i] as! UIImageView
star.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(starTap)))
star.userInteractionEnabled = true
star.tag = i
star.image = UIImage(named: (i + 1 <= grade ? "star-selected" : "star-empty"))
}
return cell
}
func changeRating(sender: UIImageView) {
let selectedStarIndex = sender.tag
let cell = sender.superview?.superview?.superview as! RatingTableViewCell
let model = users[cell.tag]
let stars = sender.superview?.subviews as! [UIImageView]
cell.averageLabel.text = "\(selectedStarIndex + 1)"
for i in 0...9 {
let imgName = i <= selectedStarIndex ? "star-selected" : "star-empty"
stars[i].image = UIImage(named: imgName)
}
}
func starTap(gesture: UITapGestureRecognizer) {
changeRating(gesture.view as! UIImageView)
}
The way to solve this problem is by updating the model that holds all the information for the uitableviewcell. Whenever a rating is updated fora particular cell, make sure you reflect that update in the respective object / dictionary in an array. Furthermore, if you have a customuitableviewcell, it might be a good idea to reset the stars in the "prepareForUse" function, so that way when a cell is reused it doesn't use old data.
In your comments, you said that you have an array with selected rates.But you did not show that in your code.
In my opinion, you need record indexPath too, because indexPath.row is binding with your rate data(may be grade?).The best way to do so is that #Jay described up.And you should not write the code of configuring cell data and cell's logic in your view controller.If your business logic is complex, you will find that it is a nightmare.^=^
Related
First let me say this seems to be a common question on SO and I've read through every post I could find from Swift to Obj-C. I tried a bunch of different things over the last 9 hrs but my problem still exists.
I have a vc (vc1) with a collectionView in it. Inside the collectionView I have a custom cell with a label and an imageView inside of it. Inside cellForItem I have a property that is also inside the the custom cell and when the property gets set from datasource[indePath.item] there is a property observer inside the cell that sets data for the label and imageView.
There is a button in vc1 that pushes on vc2, if a user chooses something from vc2 it gets passed back to vc1 via a delegate. vc2 gets popped.
The correct data always gets passed back (I checked multiple times in the debugger).
The problem is if vc1 has an existing cell in it, when the new data is added to the data source, after I reload the collectionView, the label data from that first cell now shows on the label in new cell and the data from the new cell now shows on the label from old cell.
I've tried everything from prepareToReuse to removing the label but for some reason only the cell's label data gets confused. The odd thing is sometimes the label updates correctly and other times it doesn't? The imageView ALWAYS shows the correct image and I never have any problems even when the label data is incorrect. The 2 model objects that are inside the datasource are always in their correct index position with the correct information.
What could be the problem?
vc1: UIViewController, CollectionViewDataSource & Delegate {
var datasource = [MyModel]() // has 1 item in it from viewDidLoad
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}
// delegate method from vc2
func appendNewDataFromVC2(myModel: MyModel) {
// show spinner
datasource.append(myModel) // now has 2 items in it
// now that new data is added I have to make a dip to fb for some additional information
firebaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let dict = snapshot.value as? [String: Any] else { }
for myModel in self.datasource {
myModel.someValue = dict["someValue"] as? String
}
// I added the gcd timer just to give the loop time to finish just to see if it made a difference
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.datasource.sort { return $0.postDate > $1.postDate } // Even though this sorts correctly I also tried commenting this out but no difference
self.collectionView.reloadData()
// I also tried to update the layout
self.collectionView.layoutIfNeeded()
// remove spinner
}
})
}
}
CustomCell Below. This is a much more simplified version of what's inside the myModel property observer. The data that shows in the label is dependent on other data and there are a few conditionals that determine it. Adding all of that inside cellForItem would create a bunch of code that's why I didn't update the data it in there (or add it here) and choose to do it inside the cell instead. But as I said earlier, when I check the data it is always 100% correct. The property observer always works correctly.
CustomCell: UICollectionViewCell {
let imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
let priceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var someBoolProperty = false
var myModel: MyModel? {
didSet {
someBoolProperty = true
// I read an answer that said try to update the label on the main thread but no difference. I tried with and without the DispatchQueue
DispatchQueue.main.async { [weak self] in
self?.priceLabel.text = myModel.price!
self?.priceLabel.layoutIfNeeded() // tried with and without this
}
let url = URL(string: myModel.urlStr!)
imageView.sd_setImage(with: url!, placeholderImage: UIImage(named: "placeholder"))
// set imageView and priceLabel anchors
addSubview(imageView)
addSubview(priceLabel)
self.layoutIfNeeded() // tried with and without this
}
}
override func prepareForReuse() {
super.prepareForReuse()
// even though Apple recommends not to clean up ui elements in here, I still tried it to no success
priceLabel.text = ""
priceLabel.layoutIfNeeded() // tried with and without this
self.layoutIfNeeded() // tried with and without this
// I also tried removing the label with and without the 3 lines above
for view in self.subviews {
if view.isKind(of: UILabel.self) {
view.removeFromSuperview()
}
}
}
func cleanUpElements() {
priceLabel.text = ""
imageView.image = nil
}
}
I added 1 breakpoint for everywhere I added priceLabel.text = "" (3 total) and once the collectionView reloads the break points always get hit 6 times (3 times for the 2 objects in the datasource).The 1st time in prepareForReuse, the 2nd time in cellForItem, and the 3rd time in cleanUpElements()
Turns out I had to reset a property inside the cell. Even though the cells were being reused and the priceLabel.text was getting cleared, the property was still maintaining it's old bool value. Once I reset it via cellForItem the problem went away.
10 hrs for that, smh
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.someBoolProperty = false
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}
I'm trying to do something very basic, but the fix proposed in other similar questions does not seem to be working. I have an image cache, and a tableView. I want to display the image from the cache if it exists, otherwise there should be nothing. For some reason the tableView is still displaying a reused cell with the wrong image, even when I set the image view to nil. Below is my code:
let cell = tableView.dequeueReusableCell(withIdentifier: "searchCell", for: indexPath) as! SearchResultsTableViewCell
cell.profilePhoto?.image = nil
cell.profilePhoto?.backgroundColor = UIColor.gray
if let userID = myObject.posterId, let profileImage = self.imageCache.object(forKey: userID as AnyObject) {
cell.profilePhoto?.image = profileImage
} else {
if let userId = myObject.posterId {
downloadImage.beginImageDownload() {
(imageOptional) in
if let image = imageOptional {
cell.profilePhoto?.image = image
self.imageCache.setObject(image, forKey: userId as AnyObject)
}
}
}
}
What am I doing wrong? I can't for the life of me figure out why the image is not being set to nil, even though I do that as the first step!
The problem is downloadImage.beginImageDownload closures are holding references to the uitableview cells.
When you complete the image download, you set cell.profilePhoto?.image property, even if the tableView recycles reusable cells to display a different row.
Assign your cell's tag to the indexPath.row and test if cell is still relevant for assigning downloaded image:
/* right after cell dequeue */
cell.tag = indexPath.row
then
/* download finished here */
if cell.tag == indexPath.row {
/* yeah, I want to rock this cell with my downloaded image! */
cell.profilePhoto?.image = downloadedImage
}
Be aware: this will only work in tableview with one section.
P.S. You can place the clean up of your cells in prepareForReuse method inside the SearchResultsTableViewCell to tidy things a little.
You appear to be setting your image to nil but have you considered the downloads which might be in flight when you reuse that cell? It looks like you could be updating a cell's image when the download for some previous index path finishes.
I am programmatically creating cells and adding a delete button to each one of them. The problem is that I'd like to toggle their .hidden state. The idea is to have an edit button that toggles all of the button's state at the same time. Maybe I am going about this the wrong way?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("verticalCell", forIndexPath: indexPath) as! RACollectionViewCell
let slide = panelSlides[indexPath.row]
cell.slideData = slide
cell.slideImageView.setImageWithUrl(NSURL(string: IMAGE_URL + slide.imageName + ".jpg")!)
cell.setNeedsLayout()
let image = UIImage(named: "ic_close") as UIImage?
var deleteButton = UIButton(type: UIButtonType.Custom) as UIButton
deleteButton.frame = CGRectMake(-25, -25, 100, 100)
deleteButton.setImage(image, forState: .Normal)
deleteButton.addTarget(self,action:#selector(deleteCell), forControlEvents:.TouchUpInside)
deleteButton.hidden = editOn
cell.addSubview(deleteButton)
return cell
}
#IBAction func EditButtonTap(sender: AnyObject) {
editOn = !editOn
sidePanelCollectionView.reloadData()
}
I think what you want to do is iterate over all of your data by index and then call cellForItemAtIndexPath: on your UICollectionView for each index. Then you can take that existing cell, cast it to your specific type as? RACollectionViewCell an then set the button hidden values this way.
Example (apologies i'm not in xcode to verify this precisely right now but this is the gist):
for (index, data) in myDataArray.enumerated() {
let cell = collectionView.cellForRowAtIndexPath(NSIndexPath(row: index, section: 0)) as? RACollectionViewCell
cell?.deleteButton.hidden = false
}
You probably also need some sort of isEditing Boolean variable in your view controller that keeps track of the fact that you are in an editing state so that as you scroll, newly configured cells continue to display with/without the button. You are going to need your existing code above as well to make sure it continues to work as scrolling occurs. Instead of creating a new delete button every time, you should put the button in your storyboard and set up a reference too and then you can just use something like cell.deleteButton.hidden = !isEditing
I implemented an NSFetchedResultsController on a UITableView in a Core Data project in Swift 2.0. Additionally, I have a UISearchController implemented. Everything works perfectly with the exception of the behavior I'm encountering on my custom UITableViewCell buttons.
When UISearchController is active, the customTableViewCell's buttons work as they should. If I click the same button when the fetchedResultsController is displaying its results, the method thinks Index 0 is the sender, regardless of which button I click.
func playMP3File(sender: AnyObject) {
if resultsSearchController.active {
// ** THIS WORKS **
// get a hold of my song
// (self.filteredSounds is an Array)
let soundToPlay = self.filteredSounds[sender.tag]
// grab an attribute
let soundFilename = soundToPlay.soundFilename as String
// feed the attribute to an initializer of another class
mp3Player = MP3Player(fileName: soundFilename)
mp3Player.play()
} else {
// ** THIS ALWAYS GETS THE OBJECT AT INDEX 0 **
let soundToPlay = fetchedResultsController.objectAtIndexPath(NSIndexPath(forRow: sender.tag, inSection: (view.superview?.tag)!)) as! Sound
// OTHER THINGS I'VE TRIED
// let soundToPlay = fetchedResultsController.objectAtIndexPath(NSIndexPath(forRow: sender.indexPath.row, inSection: (view.superview?.tag)!)) as! Sound
// let soundToPlay: Sound = fetchedResultsController.objectAtIndexPath(NSIndexPath(index: sender.indexPath.row)) as! Sound
let soundFilename = soundToPlay.soundFilename as String
mp3Player = MP3Player(fileName: soundFilename)
mp3Player.play()
}
}
Here's an abbreviated version of my cellForRowAtIndexPath to show I'm setting up the cells' buttons:
let customCell: SoundTableViewCell = tableView.dequeueReusableCellWithIdentifier("customCell", forIndexPath: indexPath) as! SoundTableViewCell
if resultsSearchController.active {
let sound = soundArray[indexPath.row]
customCell.playButton.tag = indexPath.row
} else {
let sound = fetchedResultsController.objectAtIndexPath(indexPath) as! Sound
customCell.playButton.tag = indexPath.row
}
// add target actions for cells
customCell.playButton.addTarget(self, action: "playMP3file:", forControlEvents: UIControlEvents.TouchUpInside)
I've tried a few other approaches I've found here, such as translating CGPoints to IndexPaths, etc. without much luck. Everything that looked promising in the compiler crashed when I clicked the button in the simulator.
Thank you for reading.
Update
Installed Xcode 7.1, rebooted, cleaned caches, nuked derived data, did a cold boot.
Solution
Tags will get the job done in many cases (such as getting the location in an Array) and get lots of votes here, but as I've learned, they don't work all the time. Thank you to Mundi for pointing me towards a more robust solution.
// this gets the correct indexPath when resultsSearchController is not active
let button = sender as! UIButton
let view = button.superview
let cell = view?.superview as! SoundTableViewCell
let indexPath: NSIndexPath = self.tableView.indexPathForCell(cell)!
let soundToPlay = fetchedResultsController.objectAtIndexPath(indexPath) as! Sound
I've tried a few other approaches I've found here, such as translating CGPoints to IndexPaths, etc. without much luck.
Translating points is indeed the most robust solution. This answer contains the correct code.
i have a TableView with a customCell. I set the values of the customCell Elements inside cellForRowAtIndexPath:. No problem. But i want to change some customCell Element Values outside of the cellForRowAtIndexPath: Scope. For example after a Swipe i want to change the Value of a cell element inside my swipe function
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell:customCell = self.tableView?.dequeueReusableCellWithIdentifier("customCell")! as customCell
let rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionary
let imageSwipeLeft = UISwipeGestureRecognizer(target: self, action: "imageSwiped:")
imageSwipeLeft.direction = .Left
let urlString: NSString = rowData["testImage"] as NSString
self.indexPathArray += [indexPath]
cell.testLabel.text = "Test Label"
cell.testImage.image = image
cell.testImage.tag = indexPath.row
cell.testImage.addGestureRecognizer(imageSwipeLeft)
cell.testImage.userInteractionEnabled = true
ImageLoader.sharedLoader.imageForUrl(urlString, completionHandler:{(image: UIImage?, url: String) in
cell.testImage.image = image
cell.testImage.layer.borderWidth = 6;
cell.testImage.layer.borderColor = UIColor.whiteColor().CGColor
cell.testImage.clipsToBounds = true
cell.placeholderLoading.stopAnimating()
})
return cell
}
func imageSwiped(recognizer: UISwipeGestureRecognizer) {
let testData: NSDictionary = self.tableData[recognizer.view.tag] as NSDictionary
let imageSlide = recognizer.view as UIImageView
var imageURL = testData["image"] as String
ImageLoader.sharedLoader.imageForUrl(imageURL, completionHandler:{(image: UIImage?, url: String) in
UIView.transitionWithView(imageSlide,
duration:0.44,
options: .TransitionCrossDissolve,
animations: { imageSlide.image = image },
completion: nil)
})
let indexPath = self.indexPathArray[recognizer.view.tag] as NSIndexPath
let cell = self.tableView?.cellForRowAtIndexPath(indexPath)as customCell
cell.testLabel.text = "Test"
}
UITableView follows the Model-View-Controller pattern. According to this pattern, all your changes need to be done to the model - in other words, to the data structure that stores the information from which you populate your cell data. Once you made the change to the model, you tell the view that the data has changed, which would then show the new data.
Let's say that your cellForRowAtIndexPath function reads from an array. Your imageSwiped function should then locate the item that has been swiped, modify its entry in the array, and call either reloadData or reloadRowsAtIndexPaths.
That's it! Once you notify the table view of the reload, it would go back to the array, find the modified data, and call your cellForRowAtIndexPath to display it.
Specifically, in your code add an array called swipedCells to the same class where you declared tableData array:
var swipedCells = Boolean[](count:self.tableData.count, repeatedValue: false)
Now replace
cell.testLabel.text = "Test Label"
line with
if self.swipedCells[indexPath.row] {
cell.testLabel.text = "Swiped!"
} else {
cell.testLabel.text = "Test Label"
}
Finally, change the imageSwiped as follows:
let indexPathRow = self.indexPathArray[recognizer.view.tag] as Int
self.swipedCells[indexPathRow] = true
self.tableView?.reloadData()
This way the cells that you have swiped would continue to have a label "Swiped!" even after you scroll.
Ok after a huge amount of hours :D i solved my problem.
let indexPath = self.priceObjects[recognizer.view.tag] as NSIndexPath
let cell = self.tableView?.cellForRowAtIndexPath(indexPath)as customCell
i call this inside my imageSwipe function. Inside the cellForRowAtIndexPath function where i set the initial cell element values i store the indexPath inside an array. An i tag the Images to get a Index if the Swipe Events happens. I think it is far away from the ideal way...but works for me. I hope someday someone post the good way ;) cause this is very quick n' dirty.