I have a UICollectionView and, the collecitonView has enough items.
When 5th cell was selected by touching, I wanna cancel the 5th cell selection and back to the last selected cell.
The UICollectionView has like below options.
var lastSelectedIndexPath: IndexPath
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionView.allowsMultipleSelection = false
collectionView.allowsSelection = true
...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if shouldRollBackSelection {
// just test code
collectionView.selectItem(at: lastSelectedIndexPath, animated: false, scrollPosition: UICollectionView.ScrollPosition())
} else {
lastSelectedIndexPath = indexPath
}
}
If collectionView.selectItem(...) function is called, the last selected cell's isSelected property's didSet will be called twice.
First call is false to true, and Second call is true to false. The result, the last selected cell's isSelected property is false.
I want to rollback the selection of UICollectionView's Cell. I mean that I want to do not allow selection of speciific cell.
Make user interaction disabled for those cells which you don't want selection
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
if indexPath.row == 4 {
cell.isUserInteractionEnabled = false
}
return cell
}
It's not the best way but you can add a guard before setting the last index to prevent duplicate calls:
guard lastSelectedIndexPath != indexPath else { return }
It is better deactivate selection for cell in cellForItemAt overload with:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YourCel", for: indexPath) as! UpperCollectionViewCell
if indexPath.row == 4 {
cell.isUserInteractionEnabled = false
}
return cell
}
Related
I have a variabel selectedIndexPath, which gets the indexPath selected from previous view controller. I am getting the required backgroundColor for cell in the collection view of present view controller. But when I select another cell in the collection view, the selected cell of previous view controller remains same without being deselected. So, now I have two cells with the background color. Following is my code
var selectedIndexPath = IndexPath()
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.allowsMultipleSelection = false
self.collectionView.allowsSelection = true
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! TestCollectionViewCell
if (indexPath == selectedIndexPath)
{
cell.backgroundColor = UIColor.white
}
else
{
cell.backgroundColor = UIColor.clear
}
collectionView.allowsMultipleSelection = false
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let cell = collectionView.cellForItem(at: indexPath)
cell?.backgroundColor = UIColor.white
collectionView.allowsMultipleSelection = false
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath)
{
let cell = collectionView.cellForItem(at: indexPath)
cell?.backgroundColor = UIColor.clear
collectionView.allowsMultipleSelection = false
}
How do I deselect the cell in cellForItemAt, when I select new cell in didSelectItemAt. Thanks in advance.
First of all set allowsMultipleSelection in Interface Builder and remove all redundant occurrences of setting it in code
You have to update the selectedIndexPath variable. Manipulating the cell directly is always a bad idea.
Reloading the cells is much more reliable.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
guard selectedIndexPath != indexPath else { return }
let indexPathsToUpdate = [selectedIndexPath, indexPath]
selectedIndexPath = indexPath
tableView.reloadRows(at: indexPathsToUpdate, with: .none)
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath)
{
guard selectedIndexPath == indexPath else { return }
selectedIndexPath = IndexPath()
tableView.reloadRows(at: [indexPath], with: .none)
}
There is only one problem: If you want to have an empty selection you have to declare selectedIndexPath as optional and handle it properly.
First of all, you don't need to override didDeselect for this. All you need to do is deselect the previously selected item while selecting the current item in didSelectItem. For that you can choose to maintain states in the cell as:
func changeToSelectedState() {
self.backgroundColor = UIColor.white
}
func changeToUnselectedState() {
self.backgroundColor = UIColor.clear
}
Or you can choose to write the same in the controller itself. And then you need to perform the deselection and selection in the following manner.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let previousSelectedCell = collectionView.cellForItem(at: selectedIndexPath)
previousSelectedCell.changeToUnselectedState()
let currentCell = collectionView.cellForItem(at: indexPath)
currentCell.changeToSelectedState()
selectedIndexPath = indexPath
}
I am trying on my viewDidLoad() to fetch images from my DB and then present the images, and highlight the first one.
My code thus far is:
profile?.fetchImages(onComplete: { (errors, images) in
for error in errors {
print("Error", error)
}
for imageMap in images {
self.photos.append(imageMap.value)
}
if self.photos.count < self.MAX_PHOTOS {
self.photos.append(self.EMPTY_IMAGE)
}
DispatchQueue.main.async {
self.photoCollectionView.reloadData()
}
self.updateSlideshowImages()
let indexPath = IndexPath(row: 0, section: 0)
let cell = self.photoCollectionView.cellForItem(at: indexPath)
if cell != nil {
self.setBorder(cell!)
}
})
However, for me cell is always nil, despite images existing and being fetched, and thus the setBorder is never called. Why is the cell always nil? I just want to set the border on the first cell.
You have placed self.photoCollectionView.reloadData() in async block so the let cell = self.photoCollectionView.cellForItem(at: indexPath) will run immediately before collection view reload, thats the reason you are getting nil for first cell.
You need to make sure that after collection view reload, I mean when all collection view cells are loaded then you can get and do your operations.
Alternatively, you can do this in cellForItemAtIndexPath like below...
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellIdentifier.jobCollectionViewCellIdentifier, for: indexPath) as! <#Your UICollectionViewCell#>
if indexPath.section == 0 && indexPath.row == 0 {
//highlight cell here
}
return cell
}
And if you want to highlight cell after all images load in collection view then you need to check for datasource of collection view.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = ...
if indexPath.row == arrImages.count-1 {
let newIndexPath = IndexPath(row: 0, section: 0)
if let cellImg = self.photoCollectionView.cellForItem(at: newIndexPath) {
self.setBorder(cellImg)
}
}
}
Instead of highlighting the UICollectionViewCell in viewDidLoad() after receiving the response, highlight it in willDisplay delegate method, i.e.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
{
if indexPath.row == 0
{
cell.isHighlighted = true
}
}
And add border of cell in the isHighlighted property of UICollectionViewCell according to the cell highlight status, i.e.
class CustomCell: UICollectionViewCell
{
override var isHighlighted: Bool{
didSet{
self.layer.borderWidth = self.isHighlighted ? 2.0 : 0.0
}
}
}
You can use isSelected property the same way in case you want to change appearance of the cell based on the selection status.
Let me know if you still face any issue.
Set your cell's border in cellForRow method.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellIdentifier", for: indexPath) as! YourCustomCell
if indexPath.row == 0 {
self.setBorder(cell)
}
return cell
}
I would like to create a list of horizontal buttons in UIcollectionview. Only one and only button can be selected and it will change to red color, problem is the selected cell will be reuse then there is possibility two red buttons appear in a same time, what should I do to solve this?
Store the index of the selected row in didSelectRow , then inside cellForItemAt
var selectedIndex = -1
var urlArr = [url1,url2,url3,url4,url5]
//
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellIdentifier1, for: indexPath as IndexPath) as! imagesCollectionViewCell
let currentUrl = urlArr[indexPath.row]
if(indexPath.row == selectedIndex)
{
// red
}
else
{
// default
}
}
if you want to create multiple toggle cells in UICollectionView then use this.
var selectedIndexes = NSMutableArray()
var items = [item1,item2,item3,item4,item5]
in cellForItemAt method. check for selected indexPath.row
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: yourIdentifier, for: indexPath as IndexPath) as! yourCell
if selectedIndexes.contains(indexPath.row){
// make it active
}else{
// make it default
}
}
And in didSelectItemAt method add and remove indexPath from the Array to toggle the button of cell
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if selectedIndexes.contains(indexPath.row){
selectedIndexes.add(indexPath.row)
}else{
selectedIndexes.remove(indexPath.row)
}
collectionView.reloadData()
}
I have UICollectionView 2 rows 10+ cells.
deselected by default. when I click it becomes selected but when I click again not deselect.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath)
let cell = collectionView.cellForItem(at: indexPath)
let collectionActive: UIImageView = {
let image=UIImageView(image: #imageLiteral(resourceName: "collectionActive"))
image.contentMode = .scaleAspectFill
return image
}()
let collectionInactive: UIImageView = {
let image=UIImageView(image: #imageLiteral(resourceName: "collectionInactive"))
image.contentMode = .scaleAspectFill
return image
}()
if cell?.isSelected == true {
cell?.backgroundView = collectionActive
}else{
cell?.backgroundView = collectionInactive
}
}
how fix that problem?
in viewDidLoad()
collectionView.allowsMultipleSelection = true;
afterword I implemented these methods
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! MyCell
cell.toggleSelected()
}
func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! MyCell
cell.toggleSelected()
}
finally in my class
class MyCell : UICollectionViewCell {
func toggleSelected ()
{
if (selected){
backgroundColor = UIColor.redColor()
}else {
backgroundColor = UIColor.whiteColor()
}
}
}
For Swift 5 +
in viewDidLoad()
collectionView.allowsMultipleSelection = true
afterword I implemented these methods
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! MovieDetailsDateCollectionViewCell
cell.toggleSelected()
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! MovieDetailsDateCollectionViewCell
cell.toggleSelected()
}
In TableView Cell class
class MyCell : UICollectionViewCell {
func toggleSelected ()
{
if (isSelected){
backgroundColor = .red
}else {
backgroundColor = .white
}
}
}
If you don't want to enable multiple selection and only want one cell to be selected at a time, you can use the following delegate instead:
If the cell is selected then this deselects all cells, otherwise if the cell is not selected, it selects it as normal.
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
let cell = collectionView.cellForItem(at: indexPath) as! CustomCell
if cell.isSelected {
collectionView.selectItem(at: nil, animated: true, scrollPosition: [])
return false
}
return true
}
According to UICollectionView class doc, you can use:
var selectedBackgroundView: UIView? { get set }
You can use this view to give the cell a custom appearance when it is selected. When the cell is selected, this view is layered above the backgroundView and behind the contentView.
In your example in the cellForItem(at indexPath: IndexPath) -> UICollectionViewCell? function you can set:
cell.backgroundView = collectionInactive
cell.selectedBackgroundView = collectionActive
If the cell is selected, just set cell.isSelected = false in shouldSelectItemAt delegate and in a DispatchQueue.main.async { } block. So the state is actually changed to false (very) soon after the shouldSelectItemAt has been executed.
It may look like a hack but it actually works.
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
if let cell = collectionView.cellForItem(at: indexPath), cell.isSelected {
DispatchQueue.main.async { // change the isSelected state on next tick of the ui thread clock
cell.isSelected = false
self.collectionView(collectionView, didDeselectItemAt: indexPath)
}
return false
}
return true
}
Please let me know if you find/know any cons to do this. Thanks 🙏
In iOS 14 and newer, you can set the backgroundConfiguration property of a cell. Once set, all necessary visual effects for selecting and deselecting works automatically. You can use one of the preconfigured configurations, like this:
cell.backgroundConfiguration = .listSidebarCell()
…or create a UIBackgroundConfiguration object from scratch. You can also change a preconfigured configuration before applying.
More info here: https://developer.apple.com/documentation/uikit/uibackgroundconfiguration
I'm trying to implement this feature: in my app, if I selected a cell in UICollectionView, then the borders becomes blue, and if I select another one, the previous should be deselected and borders should become transparent. There is methods that I've written:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ChatCell
/* Set some settings */
if globalSelected[indexPath.item] {
cell.circleView.layer.borderColor = UIColor.blue.cgColor
} else {
cell.circleView.layer.borderColor = UIColor.clear.cgColor
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//Global variable for maintain selection
global.selectedChatPath = indexPath
globalSelected[indexPath.item] = true
collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if indexPath != nilPath {
globalSelected[indexPath.item] = false
collectionView.reloadData()
}
}
The nilPath is just IndexPath(item: -1, section: 0), but it doesn't matter, because collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) isn't even called. My CollectionView has allowSelection = true and allowsMultipleSelection = false properties. I will be thankful for any help.
If only a single cell is supposed to be selected at the same time I recommend to put the currently selected index path into a instance variable (nil means nothing is selected)
var selectedIndexPath : IndexPath?
In cellForItemAtset the colors depending on the instance variable
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ChatCell
/* Set some settings */
if let selected = selectedIndexPath, selected == indexPath {
cell.circleView.layer.borderColor = UIColor.blue.cgColor
} else {
cell.circleView.layer.borderColor = UIColor.clear.cgColor
}
return cell
}
In didSelectItemAt reload only the previous and new selected cells and set selectedIndexPath to the new selected index path. This is more efficient than reloading the entire collection view.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//Global variable for maintain selection
var cellsToReload = [indexPath]
if let selected = selectedIndexPath {
cellsToReload.append(selected)
}
selectedIndexPath = indexPath
collectionView.reloadItems(at: cellsToReload)
}
didDeselectItemAt is only needed if you want to deselect a cell explicitly.
Try this
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ChatCell
/* Set some settings */
if globalSelected[indexPath.item] {
cell.circleView.layer.borderColor = UIColor.blue.cgColor
collectionView.selectItemAtIndexPath(indexPath, animated: false, scrollPosition: .None)
}
else {
cell.circleView.layer.borderColor = UIColor.clear.cgColor
collectionView.deselectItemAtIndexPath(indexPath, animated: false)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! CollectionViewCell
if selectedIndex == indexPath.item {
cell.backgroundColor = .blue
} else {
cell.backgroundColor = .clear
}
}
and in didSelectItemAt,
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedIndex = indexPath.item
collectionView.reloadData()
}
Reload your UICollectionView everytime a cell is selected and then change the border of the desired cell.
When you reload data the border for the previous cell is removed, after which you can add border to the cell that you want.
didDeselectItem won't be called until you tap the selected cell again. In order to deselect the previously selected cell when you tap another cell, you need to set the previously selected cell's setting in the global variable to false in didSelectItem like the following:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
globalSelected[global.selectedChatPath.item] = false //Set the previously selected cell's setting to false
//Global variable for maintain selection
global.selectedChatPath = indexPath
globalSelected[indexPath.item] = true
collectionView.reloadData()
}