I want to resize a particular cell in my collection view when it is selected. I found how to do so from previous posts, but ran into a problem. I found while debugging that the selection lagged behind by one, but I am not sure why.
For example, if I select one cell by tapping it, nothing happens. If I select another cell after, then the first cell I selected is enlarged. If I select a third cell, the second is enlarged. And so on.
This is how I implemented it, and only once cell is ever enlarged at the same time like I want:
var selectedIndexPath: NSIndexPath!
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
if selectedIndexPath != nil { //We know that we have to enlarge at least one cell
if indexPath == selectedIndexPath {
return CGSize(width: self.gallery.frame.size.width, height: self.gallery.frame.size.width)
}
else {
return CGSize(width: self.gallery.frame.size.width/3.0, height: self.gallery.frame.size.width/3.0)
}
}
else {
return CGSize(width: self.gallery.frame.size.width/3.0, height: self.gallery.frame.size.width/3.0)
}
}
func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
if selectedIndexPath != nil && selectedIndexPath == indexPath
{
selectedIndexPath = nil //Trigger large cell set back to normal
}
else {
selectedIndexPath = indexPath //User selected cell at this index
}
collectionView.reloadData()
}
I found while debugging that the selection lagged in didDeselectItemAtIndexPath in the way I described above, but am not sure why.
The problem is that you are're using func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath).
Pay attention to this phrase: didDeselectItemAtIndexPath.
didSelectItemAtIndexPath is a way to go.
Related
I have a collection view, and you can select the items in it and toggle them on and off by changing the background colour. The cells are toggled on/off thanks to a boolean I have in an arrow I made for all of the cells. I have saved the bool value but when I try to write them back into the array and use collectionView.reloadData()the app crashes. My collectionViewcode is:
extension OLLViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { //set the amount of items in the CollectionView to the amount of items in the OLLData dictionary
return OLLData.OLLCasesList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { //set each cell to a different mamber of the dict.
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OLLCell", for: indexPath) as! OLLCell
cell.imageView.backgroundColor = OLLData.OLLCasesList[indexPath.item]._isSelected ? UIColor.orange : UIColor.clear //change colour if selected
let image = OLLData.OLLCasesList[indexPath.item]._imageName
cell.label.text = image
cell.imageView.image = UIImage(named: image)
let savedIsSelected = defaults.bool(forKey: Key.isSelected)
OLLData.OLLCasesList[indexPath.item]._isSelected = savedIsSelected
//collectionView.reloadData() //when uncommented it crashes the app
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { //detect if case selected and reload CollectionView
let caseName = OLLData.OLLCasesList[indexPath.item]._imageName
print(caseName, OLLData.OLLCasesList[indexPath.item]._isSelected)
OLLData.OLLCasesList[indexPath.item]._isSelected = !OLLData.OLLCasesList[indexPath.item]._isSelected
defaults.set(OLLData.OLLCasesList[indexPath.item]._isSelected, forKey: Key.isSelected)
collectionView.reloadItems(at:[indexPath])
collectionView.reloadData()
if OLLData.OLLCasesList[indexPath.item]._isSelected == true { //if the item is selected, add to selectedCases array
selectedCases.append(OLLData.OLLCasesList[indexPath.item]._id)
selectedCaseNames.append(OLLData.OLLCasesList[indexPath.item]._imageName)
print(selectedCases, selectedCaseNames) //debugging
numberOfSelectedCases.text = String(selectedCases.count)
}
else if OLLData.OLLCasesList[indexPath.item]._isSelected == false { //remove from selectedCases array
selectedCases.removeAll(where: { $0 == OLLData.OLLCasesList[indexPath.item]._id })
selectedCaseNames.removeAll(where: { $0 == OLLData.OLLCasesList[indexPath.item]._imageName })
print(selectedCases, selectedCaseNames) //debugging
numberOfSelectedCases.text = String(selectedCases.count)
}
}
._isSelectedis the boolean that says whether the cell is 'toggled'.
Any ideas would be greatly appreciated.
First of all, uncommenting that line will produce an infinite loop. cellForRowAt happens because the collection view is reloading, so calling a refresh while the collection view is refreshing is no good.
So your issue is that you don't know how to display selected cells in your collection view, right?
Here's a function that fires right before the collection view is about to display a cell:
func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath)
{
<#code#>
}
Inside this function, you should:
Cast cell into your OLLCell (safely if you want to be thorough)
Look at your data and see if the cell should be selected OLLData.OLLCasesList[indexPath.item]._isSelected
Ask your casted cell to change its colors/UI/appearance according to your ._isSelected boolean
Step 3 has a VERY important caveat. You should be changing the UI when ._isSelected is false AND when it's true. Because the collection view reuses cells, old UI state will randomly recur. So setting it every time is a good way to ensure the behavior you want.
Here's an example:
func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath)
{
//Cast the vanilla cell into your custom cell so you have access
//to OLLCell's specific functions and properties.
//Also make sure the indexPath falls in the indices of your data
if let myCastedCell = cell as? OLLCell,
0 ..< OLLData.OLLCasesList.count ~= indexPath.item
{
myCastedCell.imageView.backgroundColor = OLLData
.OLLCasesList[indexPath.item]._isSelected
? UIColor.orange
: UIColor.clear
}
}
Let's say I have a UICollectionView with a UICollectionViewFlowLayout, and my items are different sizes. So I've implemented collectionView(_:layout:sizeForItemAt:).
Now let's say I permit the user to rearrange items (collectionView(_:canMoveItemAt:)).
Here's the problem. As a cell is being dragged and other cells are moving out of its way, collectionView(_:layout:sizeForItemAt:) is called repeatedly. But it's evidently called for the wrong index paths: a cell is sized with the index path for the place it has been visually moved to. Therefore it adopts the wrong size during the drag as it shuttles into a different position.
Once the drag is over and collectionView(_:moveItemAt:to:) is called, and I update the data model and reload the data, all the cells assume their correct size. The problem occurs only during the drag.
We clearly are not being given enough information in collectionView(_:layout:sizeForItemAt:) to know what answer to return while the drag is going on. Or maybe I should say, we're being asked for the size for the wrong index path.
My question is: what on earth are people doing about this?
The trick is to implement
override func collectionView(_ collectionView: UICollectionView,
targetIndexPathForMoveFromItemAt orig: IndexPath,
toProposedIndexPath prop: IndexPath) -> IndexPath {
During a drag, that method is called repeatedly, but there comes a moment where a cell crosses another and cells are shoved out of the way to compensate. At that moment, orig and prop have different values. So at that moment you need to revise all your sizes in accordance with how the cells have moved.
To do that, you need to simulate in your rearrangement of sizes what the interface is doing as the cells move around. The runtime gives you no help with this!
Here's a simple example. Presume that the user can move a cell only within the same section. And presume that our data model looks like this, with each Item remembering its own size once collectionView(_:layout:sizeForItemAt:) has initially calculated it:
struct Item {
var size : CGSize
// other stuff
}
struct Section {
var itemData : [Item]
// other stuff
}
var sections : [Section]!
Here's how sizeForItemAt: memoizes the calculated sizes into the model:
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let memosize = self.sections[indexPath.section].itemData[indexPath.row].size
if memosize != .zero {
return memosize
}
// no memoized size; calculate it now
// ... not shown ...
self.sections[indexPath.section].itemData[indexPath.row].size = sz // memoize
return sz
}
Then as we hear that the user has dragged in a way that makes the cells shift, we read in all the size values for this section, perform the same remove-and-insert that the interface has done, and put the rearranged size values back into the model:
override func collectionView(_ collectionView: UICollectionView,
targetIndexPathForMoveFromItemAt orig: IndexPath, toProposedIndexPath
prop: IndexPath) -> IndexPath {
if orig.section != prop.section {
return orig
}
if orig.item == prop.item {
return prop
}
// they are different, we're crossing a boundary - shift size values!
var sizes = self.sections[orig.section].rowData.map{$0.size}
let size = sizes.remove(at: orig.item)
sizes.insert(size, at:prop.item)
for (ix,size) in sizes.enumerated() {
self.sections[orig.section].rowData[ix].size = size
}
return prop
}
The result is that collectionView(_:layout:sizeForItemAt:) now gives the right result during the drag.
The extra piece of the puzzle is that when the drag starts you need to save off all the original sizes, and when the drag ends you need to restore them all, so that when the drag ends the result will be correct as well.
While the accepted answer is pretty clever (props to you Matt 👍), it's actually an unnecessarily elaborate hack. There is a MUCH simpler solution.
The key is to:
Store cell sizes within the data itself.
Manipulate or "rearrange" the data at the point when the "moving" cell enters a new indexPath (NOT when the cell finishes moving).
Fetch cell sizes directly from the data (which is now properly arranged).
Here's what this would look like...
// MARK: UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// (This method will be empty!)
// As the Docs states: "You must implement this method to support
// the reordering of items within the collection view."
// However, its implementation should be empty because, as explained
// in (2) from above, we do not want to manipulate our data when the
// cell finishes moving, but at the exact moment it enters a new
// indexPath.
}
override func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
// This will be true at the exact moment the "moving" cell enters
// a new indexPath.
if originalIndexPath != proposedIndexPath {
// Here, we rearrange our data to reflect the new position of
// our cells.
let removed = myDataArray.remove(at: originalIndexPath.item)
myDataArray.insert(removed, at: proposedIndexPath.item)
}
return proposedIndexPath
}
// MARK: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// Finally, we simply fetch cell sizes from the properly arranged
// data.
let myObject = myDataArray[indexPath.item]
return myObject.size
}
Based on Matts answer I have adapted the code to fit for UICollectionViewDiffableDataSource.
Track the indexPath wile moving the cells:
/// Stores remapped indexPaths during reordering of cells
var changedIndexPaths = [IndexPath: IndexPath]()
func collectionView(_ collectionView: UICollectionView,
targetIndexPathForMoveFromItemAt orig: IndexPath,
toProposedIndexPath prop: IndexPath) -> IndexPath {
guard orig.section == prop.section else { return orig }
guard orig.item != prop.item else { return prop }
let currentOrig = changedIndexPaths[orig]
let currentProp = changedIndexPaths[prop]
changedIndexPaths[orig] = currentProp ?? prop
changedIndexPaths[prop] = currentOrig ?? orig
return prop
}
Calculate size of cells:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// remap path while moving cells or use indexPath
let usedPath = changedIndexPaths[indexPath] ?? indexPath
guard let data = dataSource.itemIdentifier(for: usedPath) else {
return CGSize()
}
// Calculate your size for usedPath here and return it
// ...
return size
}
Reset the indexPath map (changedIndexPaths) after final movement of cell is finished:
class DataSource: UICollectionViewDiffableDataSource<Int, Data> {
/// Is called after an cell item was successfully moved
var didMoveItemHandler: ((_ sourceIndexPath: IndexPath, _ target: IndexPath) -> Void)?
override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
didMoveItemHandler?(sourceIndexPath, destinationIndexPath)
}
}
dataSource.didMoveItemHandler = { [weak self] (source, destination) in
self?.dataController.reorderObject(sourceIndexPath: source, destinationIndexPath: destination)
self?.resetProposedIndexPaths()
}
func resetProposedIndexPaths() {
changedIndexPaths = [IndexPath: IndexPath]() // reset
}
My collection view is re-orderable since using LXReorderableCollectionViewFlowLayout for its flowLayout object, and I don't want my collection view's section header to respond long-press touch. But I can't check if it's section header or cell in following delegate call.
func collectionView(collectionView: UICollectionView, canMoveItemAtIndexPath indexPath: NSIndexPath) -> Bool {
}
Any idea?
You can put a check to see what kind of cell user is long pressing:
func collectionView(collectionView: UICollectionView, canMoveItemAtIndexPath indexPath: NSIndexPath) -> Bool {
if let cell = collectionView.cellForItemAtIndexPath(indexPath) as? SectionClassName {
return false
}
}
I have a weird issue with UICollectionView and UITabBarController. Inside the UITabBarController i have references to two different ViewControllers. If i put View Controller that has UICollectionView as a first page of UITabBarController then my list with cells (UICollectionView) is loading normally like this:
But if i put it as a second View Controller when i open the tab it is like UIImageView and UILabel are disappearing from the cells:
I have checked collectionView method that is getting the cells and there is always data printed for the Label and Image. Here is a code of the methods for DataSource and Delagate
// tell the collection view how many cells to make
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.items.count
}
// make a cell for each cell index path
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! AdsCollectionViewCell
// Use the outlet in our custom class to get a reference to the UILabel in the cell
cell.myLabel.text = self.items[indexPath.item]
cell.backgroundColor = UIColor.whiteColor() // make cell more visible in our
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.layer.cornerRadius = 8
cell.layoutIfNeeded()
return cell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let width = collectionView.frame.width - 22;
return CGSize(width: width/2, height: width/2);
}
// MARK: - UICollectionViewDelegate protocol
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// handle tap events
print("You selected cell #\(indexPath.item)!")
}
Is there someone that had problem like this? How can it be solved?
I currently have a collection view set up to display a dynamic number of objects in its view. Each cell displays an image from the corresponding object. When a cell is tapped, it triggers a segue to the next view in the hierarchy. The cell's corresponding object is passed to the next view However, I am noticing that when I return the view with the collection, the ordering of cells has changed, and now, when I tap one to got the next view, its properties are from objects of other cells.
Below are my methods of the UICollectionView:
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return objectsCount
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Chow Object Reuse ID", forIndexPath: indexPath) as UICollectionViewCell
cell.backgroundColor = UIColor.whiteColor()
var imageView:UIImageView = UIImageView()
imageView.frame = CGRect(x: 0, y: 0, width: 90, height: 90)
//Since loading of images is a time-intensive task, all the thumbnails
//may have not been fetched yet.
if (imageThumbsArray.count == objectsCount) { //Eventually, a more elegant fix will be needed.
imageView.image = imageThumbsArray[indexPath.row]
}
cell.addSubview(imageView)
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
self.objectToBePassed = self.chowObjectsArray[indexPath.row] as PFObject
self.performSegueWithIdentifier("Show Chow Details", sender: self)
}
I also call self.PastObjectsCollection.reloadData()
Why is the reordering and mixing up of cells happening?
Thanks,
Siddharth