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.
Related
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
I'm using a collection view and every time I select the cell I change the cell background color to red. Simple enough:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)! as! CustomCell
cell.backgroundColor = .red
}
This works absolutely fine. When I select the top 3 cells going from left to right, the background color changes exactly as I expect:
However If I reload the collectionView after I select the cell the selection ordering begins to behave strangely. When I select the same top 3 cells in the same order from left to right, different cells become selected:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)! as! CustomCell
cell.backgroundColor = .red
collectionView.reloadData()
}
Apple's documentation is cryptic. https://developer.apple.com/documentation/uikit/uicollectionview/1618078-reloaddata
They say that "This causes the collection view to discard any currently visible items (including placeholders) and recreate items based on the current state of the data source object. " But this makes me think that upon calling reloadData() the collectionViewCells would go back to gray and not jump indexPaths.
Can anyone explain what is going on in reloadData() to make the cell selection at index path ordering so strange?
First You need to use dequeue cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:"CustomCell", for: indexPath) as! CustomCell
And in cell you can use
-(void)prepareForReuse {
// Set default implementation
}
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 have a collection view and when something changes I update the data source and reload the cell in which the change occurs. The cell kind of blinks when it reloads. It doesn't really affect the user scrolling and I made it almost unnoticeable with:
UIView.performWithoutAnimation{
self.collectionView.reloadItemsAtIndexPaths([NSIndexPath(forItem: index, inSection: 0)])
}
This was the best I could do to make the reload less noticeable. I have a background image taking up the entire cell. I think the flash that I'm seeing is this image reloading, yet I don't need it to reload because the image will never change. Does anyone know how to make the cell reload but not the image? I can put a variable in there and change it such as (initalLoad = false) but I don't know how to keep the image from reloading.
Try moving all your cell setup to an internal func in your UICollectionViewCell subclass:
class MyCollectionViewCell: UICollectionViewCell {
var initialLoad = true
// since collection view cells are recycled for memory efficiency,
// you'll have to reset the initialLoad variable before a cell is reused
override func prepareForReuse() {
initialLoad = true
}
internal func configureCell() {
if initialLoad {
// set your image here
}
initialLoad = false
// do everything else here
}
}
and then calling it from your view controller:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath) as! MyCollectionViewCell
cell.configureCell()
return cell
}
You can add parameters to the configureCell() function to pass in any data you need for setting up the cell (presumably you'll need to pass in some kind of reference to your image). If you have a lot of information, you might want to create a custom object to hold all that information, then pass that into the function as a parameter.
I have a UICollectionView displaying custom cells in a horizontal flow layout; in other words, some content is placed outside the screen bounds.
Additionally, I have a gesture that fires an NSNotification leading to a color change of some of the elements of my cells (i.e. a theme). Everything works perfectly except for the fact that the cells that are present out of the bounds of the screen don't all update to the new color change. Is there any way to force them to redraw?
In the function called when the NSNotification is fired I've tried redrawing the collection view with self.collectionView.reloadData(), self.collectionView.setNeedsDisplay() and self.collectionView.setNeedsLayout but to no avail. I tried the last two of the list in the awakeFromNib() of the custom cell class but nothing.
Here is the code for my cellForItemAtIndexPath:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = popularCollectionView!.dequeueReusableCellWithReuseIdentifier("popular", forIndexPath: indexPath) as! PopularPainting
cell.thumbnail.image = paintings[indexPath.row].paintingImage
cell.name!.text = paintings[indexPath.row].paintingName
cell.price!.setTitle(paintings[indexPath.row].paintingPrice, forState: UIControlState.Normal)
if cell.isDark {
cell.name!.textColor = UIColor(red: 205/255, green: 205/255, blue: 205/255, alpha: 1)
cell.price!.setTitleColor(self.kPaleBlue, forState: .Normal)
self.popularCollectionView.reloadData()
}
return cell
}
Any suggestions?
Note: Scrolling to the offscreen content and repeating the gesture to change themes works perfectly so I have no idea what's up.
Your assumption that there are cells that exist off-screen for every item in your collection view is incorrect. In fact, table views and collection views re-use cells that get scrolled off-screen for new cells coming on-screen, so there are only ever just over a screenful of cells in existence.
You're right to call reloadData after the notification fires. But you need to ensure your implementation of collectionView:itemForRowAtIndexPath: will correctly configure cells that are scrolled on-screen subsequently. That will probably mean saving the state change in a property after the notification fires, and checking that property when you configure cells in collectionView:itemForRowAtIndexPath:.
To get around the "view cannot be reached using cellForItemAtIndexPath:, but will not be recycled before being shown on-screen" issue, you can move the view initialization logic from collectionView:cellForItemAtIndexPath: to collectionView:willDisplayCell:forItemAtIndexPath:.
For example, where you originally had:
override func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: self.reuseIdentifier,
for: indexPath
)
// Initialize cell
return cell
}
You can replace it with:
override func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(
withReuseIdentifier: self.reuseIdentifier,
for: indexPath
)
}
override func collectionView(
_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath
) {
// Initialize cell
}
This ensures that if cellForItemAtIndexPath: returns nil, the cell will be properly initialized by collectionView:willDisplayCell:forItemAtIndexPath: the next time before it is shown on screen.