I have two sections in my UICollectionView. I would like to be able to drag cells within each section but not between them.
I am using a long press gesture recognizer to animate the cell drag movement so I could check that the drop location is not in a different section. Is there a way to determine a section's frame?
Or is there a simpler way?
In the UICollectionViewDropDelegate, this protocol func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal can help with it.
Check the sample below for how I prevent an item being dragged from one section to the other section:
In UICollectionViewDragDelegate, we use the itemsForBeginning function to pass information about the object. You can see that, I passed the index and item to the localObject
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let item = sectionViewModel[indexPath.section].items[indexPath.row]
let itemProvider = NSItemProvider(object: item.title as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = (item, indexPath)
return [dragItem]
}
In UICollectionViewDropDelegate, I did this:
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
if let object = session.items.first?.localObject as? (Item, IndexPath), object.0.status, let destinationIndexPath = destinationIndexPath, object.1.section == destinationIndexPath.section {
let itemAtDestination = sectionViewModel[destinationIndexPath.section].items[destinationIndexPath.row]
if itemAtDestination.status {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
}
return UICollectionViewDropProposal(operation: .forbidden)
}
According to Apple:
While the user is dragging content, the collection view calls this method repeatedly to determine how you would handle the drop if it occurred at the specified location. The collection view provides visual feedback to the user based on your proposal.
In your implementation of this method, create a UICollectionViewDropProposal object and use it to convey your intentions. Because this method is called repeatedly while the user drags over the table view, your implementation should return as quickly as possible.
What I Did:
I've a couple of restrictions to cover:
Prevent item.status == true from going to items within the same section
Prevent item from going to other sections
GIF
There is a very simple solution. I have tried it in tableView - but suppose it works fine with Collections also. Though, it's not "a clean and swifty one" - so I would suggest to use it as a temporary instrument:
Inside moveRowAt function you check, if section in sourceIndexPath matches a section in destinationIndexPath. If sections are the same - you let the method to go on. If sections in are different - the function will stop.
This is how it looks like in code:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let sourceSection = sourceIndexPath.section
let destinationSection = destinationIndexPath.section
if sourceSection == destinationSection {
// Here you can put your code, e.x:
//
//let removedItem = yourArray.remove(at: sourceIndexPath.row)
//yourArray.insert(removedItem, at: destinationIndexPath.row)
} else {
//print ("not this time")
}
}
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
}
I'm using collection view and want to move cells when user paned. This is the code.
func longPressPhotoCollectionView(sender: UILongPressGestureRecognizer) {
guard isEditing else {
return
}
let point = sender.location(in: photoCollectionView)
switch sender.state {
case .began:
guard let indexPath = photoCollectionView.indexPathForItem(at: point) else {
return
}
let isS = photoCollectionView.beginInteractiveMovementForItem(at: indexPath)
print(isS)
case .changed:
photoCollectionView.updateInteractiveMovementTargetPosition(point)
case .ended:
photoCollectionView.endInteractiveMovement()
default:
photoCollectionView.cancelInteractiveMovement()
}
}
I doubt custom layout will cause this problem. And I found this from Apple's Doc.
When you call this method, the collection view consults its delegate to make sure the item can be moved. If the data source does not support the movement of the item, this method returns false.
I don't know how to handle animations, especially with custom layout. Please help me! Thanks!
Implement the following in your UICollectionViewDataSource:
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
print("MOVING!")
// Switch the Items in the DataSource
}
When you call this method, the collection view consults its delegate to make sure the item can be moved. If the data source does not support the movement of the item, this method returns false.
The Apple documentation is misleading. It seems to imply that you should override some method on UICollectionViewDelegate to make a cell movable. In fact, it's a UICollectionViewDataSource method:
optional func collectionView(_ collectionView: UICollectionView,
canMoveItemAt indexPath: IndexPath) -> Bool
https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618015-collectionview
I am using the viewForSupplementaryElementOfKind function to apply the header section from the uicollectionview controller. However, before the asynchronous parsing of the viewDidAppear API, the row index is loaded into the viewForSupplementaryElementOfKind function and becomes out of range. What should I do?
Here is my code...
override func viewDidAppear(_ animated: Bool) {
callVideo3API()
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let row1 = self.list[0]
let row2 = self.list[1]
let row3 = self.list[2]
let headerSection = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "Header", for: indexPath) as! HeaderSection
headerSection.nameLabel01.text = row1.nickname
headerSection.nameLabel02.text = row2.nickname
headerSection.nameLabel03.text = row3.nickname
return headerSection
default:
assert(false, "Unexpected element kind")
}
}
You must wait until the callVideo3API() get completed. After successful completion of callVideo3API() you can reload the collection view to get the output. Please follow below steps
Call method callVideo3API()
Make CollectionView empty by returning zero through CollectionView
DataSource [func numberOfSections(in collectionView:
UICollectionView) -> Int, func collectionView(_ collectionView:
UICollectionView, numberOfItemsInSection section: Int) -> Int ]
(Optional) On the time of callVideo3API() execution you can a show an activity indicator on place of your CollectionView
After successful completion of callVideo3API() you can reload
CollectionView with corresponding DataSource Value. This time it
will work without any fault :-) (If you put activity indicator don't forget to remove after successful api call)
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
}
}