I have a (horizontal) collectionView which is in tableViewCell.
My tableViewCell row has a label and collectionView
Collectionview has button in each cell
When button is tapped, I highlight the button's color and tableCell's row which is perfectly working. I have problem with prepareForReuse()
Scenario: I tapped on button and it changes its color(in collectionCell) and highlights tableViewCell's row and
I have 10+ tablerows.
When I scroll down tableView, I see the selection I made on tableRow1 appears in tableRow10
I added prepareForReuse() in tableviewcell but how can i reference and reset the button appearance which is inside collectionview from table's prepareForReuse()
Please advice
// This is my tableCell reuse method
override func prepareForReuse() {
super.prepareForReuse()
tableCellLabel.text = nil
// Reset collectionViewCell button display here
}
// This is my collectionView Datasource method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ActivitySlotCollectionViewCell.identifier, for: indexPath) as! ActivitySlotCollectionViewCell
cell.configure(from: dataModel)
return cell
}
// This is my collectionViewCell reuseMethod
override func prepareForReuse() {
super.prepareForReuse()
isButtonSelected = false
slotButton.setTitleColor(UIColor.blue, for: .normal)
slotButton.layer.borderColor = isButtonSelected ? UIColor.white.cgColor : UIColor.black.cgColor
slotButton.backgroundColor = isButtonSelected ? UIColor.red : UIColor.clear
}
Check the below link. Each time we dequeue a reuseable cell we need to clean it, actually this last disappeared cell. For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view' s delegate in tableView(_:cellForRowAt:) should always reset all content when reusing a cell.
Check this link
almost same for tableView and collectionView
Try to add this inside the prepareForReuse() function because basically you're not setting the default design for the cell that's being reuse that is why you see the selection you made on tableRow1 appears in tableRow10
slotButton.layer.borderColor = UIColor.black.cgColor
slotButton.backgroundColor = UIColor.clear
Related
I have a UICollectionView with 4 views in it. Each of these views have a UITableView inside it with custom cells. Each cell of the UITableView has a UIButton inside it and I have 2 cells per UITableView.
Something strange is happening. I have an action function for each button so that when a button is clicked, it becomes purple. The strange thing is this: if I scroll to the 4th view of my collection view and click on a button, it becomes purple as expected but then when I scroll to the 1st view of my collection view, the same button that I clicked in the 4th view (either the first one or second one) is also purple as if the 4th view of my collection view was referencing the items of my 1st view of the collection view.
I don't know at which point the 1st view becomes the same as the 4th view but here is a sample of the code:
// this is the cellForItemAt of my UICollectionView, very basic
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cellView = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! PollCellView
return cellView
}
// THIS IS ANOTHER FILE HERE
// this is part of my view that populate the UICollectionViews
class PollCellView: UICollectionViewCell {
// the table view
let questAndAnswersTableView : UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.separatorStyle = .none
tableView.allowsSelection = false
return tableView
}()
// I add the tableview in the view here
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(questAndAnswersTableView)
// a classic cellForRowAt of my UITableView
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AnswerCell2", for: indexPath) as! AnswerCell2
return cell
}
// THIS IS ANOTHER FILE HERE
// this part is my custom cell of the UITableView
class AnswerCell2: UITableViewCell {
let answerTextButton: UIButton = {
let answerButton = UIButton()
answerButton.setTitle("initial text", for: .normal)
return answerButton
}()
// I add the button to the cell here
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String!) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(answerTextButton)
// I define the action function
answerTextButton.addTarget(self, action: #selector(answerClicked), for: .touchUpInside)
}
// the action to make the button purple
#objc func answerClicked(sender: UIButton) {
sender.backgroundColor = UIColor(displayP3Red: 0.6902, green: 0.7176, blue: 0.9922, alpha: 1.0)
}
[EDIT FOLLOWING THE ANSWERS RECEIVED]
Dequeuing is definitely not as simple as it first seems... You can't trust that the tableview dequeued in a given collection view is really the one you expect... You need to keep track of the content (model) yourself. One thing that made it easier to fix is to use closures between the cell of the TableView and the cell of the UICollectionViewCell... You can very easily pass data from one to the other (like what indexPath was clicked, etc.).
when you're managing multiple nested tableviews or collectionviews or even when you're managing this yourself, you have to set up a system of arrays that when the button is clicked, you add those indexpaths to an array called "indexPathsThatArePuple" then when you after each button click, inside the button click function, you pass the indexPath and add that to the array. If the button is already inside the array when the button click function is pressed, then you remove that indexPath. in the button click function, after you add or remove indexPaths, you reload the collection view which will reload the tableviews and then inside "cellForItemAtIndexPath" you check the indexPathsThatArePuple array and then write "if indexpath is contained in indexPathsThatArePuple" then set the cell color to purple.
I understand what you're trying to do, but you don't realize or understand just how complicated the cell reuse system is in iOS. The first thing you need to do is wrap your mind around the idea the that you have to manually manage cell states due to cell resuse. The purple cell showing up in another table is from cell reuse. Apple won't automatically manage this for you, you have to do it yourself. As i described above and even my description above is not that cut and dry since you'll likely struggle with this for weeks until you grasp the concept. Good luck, you'll eventually get it.
It seems to be caused by UICollectionView cell reuse mechanism.
When the cell is created and scroll out of the screen, it will be reused later if a new cell with the same identifier is required. In your case, when you scroll down, the 1st view is reused as the 4th view, And when you scroll to the top, The 4th view is reused as the first view. If the method prepareForReuse happens to be not implemented, the UI status of the cell will not be change, so you will see the button with purple color.
I added a change theme button in my main view controller where I have a tiny table view. I can change every color except a table view cell's Content View background color, and I can't access it outside cellForRowAt.
What should I do? Is there anyway to trigger a function from my custom Cell to change the color?
You can add a property in your controller to determine the color of the cells. When you wanna change the color, you call tableView.reloadData(). This will make cellForRowAt be called on each visible cell, and you can change color in this delegate method.
Your viewController
YourViewController: UIViewController {
fileprivate var cellColor = UIColor.blue
// where you change color
func changeColor() {
cellColor = UIColor.red
// this will make the delegate method `cellForRowAt` be called on each visible row
tableView.reloadData()
}
}
cellForRowAt:
// delegate method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuse id", for: indexPath) as! YourCustomCell
cell.contentView.backgroundColor = cellColor
// anything else...
return cell
}
You can achieve this by implementing delegate, if you have custom cell class and access the delegate method to change content view color.
I've got a collection view listing a bunch of videos and tapping any of them will push navigation controller which contain a custom player view to play the video. Tapping the close button on the customer player view will pop the current controller and go back to the video list controller.
Also when tapping one of the cells that cell will become gray color. When going back and tapping another cell from the video list, I want to deselect the previously selected cell and make it back to white and make the newly selected cell to be gray color.
The problem is, didDeselectCellAtIndexPath method is NEVER called. The previously selected cell does get deselected, which I could see from the print of the selected indexPath. However the delegation method never gets called thus backgroundColor never changes back to white. It looks like multiple cells are selected, despite allowsMultipleSesection is already set to false.
Following configuration is set:
let layout = UICollectionViewFlowLayout()
collectionView?.collectionViewLayout = layout
collectionView?.delegate = self
collectionView?.dataSource = self
collectionView?.allowsSelection = true
collectionView?.allowsMultipleSelection = false
Here is my collectionView methods and delegation methods:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! PreviewCell
cell.snapShotImageView.image = videoInfoArray[indexPath.item].previewImg
cell.durationLabel.text = videoInfoArray[indexPath.item].lengthText()
cell.dateLabel.text = videoInfoArray[indexPath.item].dateAddedText()
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! PreviewCell
cell.backgroundColor = UIColor.rgb(red: 240, green: 240, blue: 240)
let url = URL(fileURLWithPath: videoInfoArray[indexPath.item].path)
let vc = VideoController()
self.videoController = vc
vc.url = url
self.navigationController?.pushViewController(vc, animated: true)
}
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! PreviewCell
cell.backgroundColor = UIColor.white
cell.captionFileLabel.backgroundColor = .white
print("Deselect called!!! This line should be printed, but it never happens!!!!")
}
Let the cell handle its background color.
Just add the following to your "PreviewCell" class:
override var isSelected: Bool {
didSet {
// TODO: replace .red & .blue with desired colors
backgroundColor = isSelected ? .red : .blue
}
}
If the parent Class doesn't implement a delegate method, any Subclass won't be able to do it either.
Please make sure the Class you are Subclassing implements it.
From the documentation I can understand that this method gets called when the user selected cell X, and then selects cell Y. Now cell X deselected and the method will be called.
Save an index of the selected cell before you move to the new view controller, and when you come back to the collection view controller, deselect the cell programmatically and then run inside your own function what you wanted to run in the deselect delegate method.
The collection view calls this method when the user tries to deselect an item in the collection view. It does not call this method when you programmatically deselect items.
If you do not implement this method, the default return value is true.
didDeselectItemAt is called when allowsMultipleSelection is set to true.
backgroundColor never changes back to white
even when your previously selected cell does get deselected, because your view doesnt get updated. You need to update your collection view cells view everytime you go back. You can refresh complete UICollectionView in viewWillAppear of your collectionViewController subclass. You can also use #entire method to deselect all selected indexPath.
At the end of your didSelectItemAt method, call the deselectItem(at:animated:) method on the collection view.
I am attempting to programmatically create a button in each cell of a UICollectionView; however, only the first button is visible. I have tried adding print statements to see what subviews my cells have and the button is present but it is not appearing on the screen.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCell", for: indexPath)
// Configure the cell
let button = UIButton(frame: cell.frame)
button.addTarget(self, action: #selector(cellClicked), for: UIControlEvents.touchUpInside)
button.backgroundColor = UIColor.red
button.tag = indexPath.row
cell.addSubview(button)
print(cell.subviews)
return cell
}
Also, I added a print statement when clicking the buttons and only the first button shows up and prints out 0.
#IBAction func cellClicked(sender: UIButton) {
print(sender.tag)
}
Here is a screenshot of the collection view, there should be two buttons in the picture but only one appears
Any help is much appreciated.
It's very bad to add button in data source, because when cell reused, new buttons will be created. If you're using Interface Builder, please add button directly. And you can adjust their properties. You can also define a custom cell, and just CTRL-Drag an outlet. Or handle selection in collection view's delegate.
optional public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
Another solution is add button in cell's awakeFromNib(), this will be called only once.
I'm loading UIViews into my tableview cells content view but as I scroll through my tableview, only one cell loads its view. Its loading the correct view for each cell but it only loads one and then disappears as a new cell appears from the bottom. All of the data is loaded locally via a function makeTableViewRowView (an array of strings that populates the uiview).
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: expenseCell, for: indexPath)
tableViewRow = makeTableViewRowView(indexPath: indexPath)
tableViewRow.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(tableViewRow)
cell.selectionStyle = .none
let margins = cell.contentView.layoutMarginsGuide
tableViewRow.centerYAnchor.constraint(equalTo: margins.centerYAnchor).isActive = true
tableViewRow.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
tableViewRow.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
tableViewRow.heightAnchor.constraint(equalToConstant: 40).isActive = true
return cell
}
make custom cell in interface builder and deque it. And make changes as per requirement for that cell in cellForRowAtIndexPath! Because cellForRowAtIndexPath will get called when your cell will be deque, I mean cell will be reused! So, creating view in cellforrow and add it as subview is not good solution, you can add directly from interface builder and can manipulate it in cellforrow as per requirement!