Multiple Buttons not appearing in UICollectionViewCell - ios

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.

Related

What goes on behind the scenes in .reloadData() in terms of cell indexPath?

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
}

UICollectionView DidDeselectItemAtIndexPath method not called in Swift

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.

didSelectRowAtIndexpath not triggered for button inside a tableview cell Swift iOS

I have a button inside a tableview cell (as shown in the below image), when I click the button,
didSelectRowAt indexpath
is not being triggered, could some one please suggest how I could do this ?
Please note:
I am performing a set of actions on click of the button, in addition I would also like
didselectRowAt indexPath
to be triggered.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Table view cell has been clicked")
}
When you have multiple views in the cell they may receive the touch event and not the "cell" so didSelectRowAt will not be triggered. In that case add view.isUserInteractionEnabled = false to each view, label or button.
Put the outlet to the button inside a custom cell class. Then set tag and add selector for button in cellForItemAt. This example is for colletionView just change to suit tableView.
If you want the button in each cell this is how you have to do it. Adding a static button to a single cell won't call didSelectItemAt because your tapping a button that doesn't have reference to a reusable cells indexPath.
This way we send the button.tag tot he function so we know which cell the button is related to.
class MyClassViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
.... // Stuff
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
....// Create Cell
cell.deleteCellButton.tag = indexPath.item
cell.deleteCellButton.addTarget(self, action: #selector(MyClassViewController.deleteCellButtonTapped(_:)), for: .touchUpInside)
return cell
}
func deleteCellButtonTapped(_ sender: Any) {
... // Stuff
print("Selector called")
}
}
If you are adding button or gesture on UITableViewCell , didselectRowAt indexPath not invoked . You can add button on remaining UI UITableViewCell than clear color apply on button . User didn't show button and you can perform didselectRowAt indexPath method task . If you wanna indexpath in button method code give below.
func btnChildDropDown(sender:UIButton)
{
let cell = sender.superview?.superview as! ChildAgeTableViewCell
let indexPath = self.tblViewAddRooms.indexPath(for: cell)
}

Removing and Reloading collection view from UICollectionViewCell class

My each UICollectionViewCell class have a delete button:
#IBAction func hideSingleCampaign(sender: AnyObject) {
self.removeFromSuperview()
}
Once tapped, cell disappears, and UICollectionView is left with empty space. I have no UICollectionView reference from cell class so I can't use collectionView.reloadData()
How am I supposed to shift other cells up?
You need to remove the object from the array that you are using with your CollectionViewDataSource method instead of deleting cell, So first of all declare action of Button inside CollectionViewCell, so that add action of button in the cellForItemAtIndexPath like this.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath:NSIndexPath)->UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as CollectionCell
cell.delBtn.tag = indexPath.item
cell.delBtn.addTarget(self, action: #selector(self.onDeletTapped(_:)),
forControlEvents: .TouchUpInside)
return cell
}
Add this method onDeletTapped on your ViewController
func onDeletTapped(sender: UIButton) {
let index = sender.tag
yourArray.removeAtIndex(index)
self.collectinView.reloadData()
}

Why won't UICollectionViewCells that are offscreen update?

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.

Resources