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
Related
Currently, I have a collectionView and its only function is to let the user choose a color. When the user chooses a color (clicks a cell), a selected indicator (UIView) animates inside the cell, letting the user know that that certain color is selected. The core functionality is there, but my question is based more off of UI. When I scroll, the cell thinks I am clicking it and it sets isSelected to true for a second until I remove my finger from the cell. This causes an animation to happen at unwanted times. So, I think the problem is in shouldSelectItemAt in the CollectionViewDelegate methods. With UIGestureRecognizer I can measure whether or not the sender state has .began or .ended. Would my question call for this or something similar?
Here are the selection collectionView delegate methods.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = colorPreviewCollectionView.cellForItem(at: indexPath) as! TrayColorPreviewCell
// stuff with cell happens here
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
let cell = colorPreviewCollectionView.cellForItem(at: indexPath) as! TrayColorPreviewCell
guard cell.isSelected else { return true }
return false
}
In the TrayColorPreviewCell (custom UICollectionViewCell) class, I use the isSelected to run the animation. This animation is the one that is happening repeatedly when scrolling.
override var isSelected: Bool {
didSet {
changeSelectionStatus()
}
}
func changeSelectionStatus() {
if isSelected {
// animation to indicate selection
} else {
// animation to indicate changed selection
}
}
Hello on a specific viewController of my project i have a UICollectionView with a custom class cell. But i have a big problem this that func :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("tapped on a cell")
}
when is click on a cell and instant realease (normal click) it does nothing, just nothing.
if i press and hold about 1s without release the finger it become grey, highlighted
And if i press and hold at least 3 seconds release the finger didSelectItemAt is executed correctly.
I tried to do the same on another project and that's work great but not on this VC and i really don't find the problem. The VC Bugged is of addTest clas in Main.storyboard
The insight of Mojtaba Hosseini is very clever, but the answer given might not be quite correct.
It turns out that there is a UITapGestureRecognizer on the main view; if it recognizes before the tap on the cell, it prevents cell selection. But if you merely set cancelsTouchesInView to false on that gesture recognizer, then they both operate, and that seems unlikely to be what is wanted. We surely want the cell tap and not the tap gesture recognizer tap.
The correct solution is thus to give the tap gesture recognizer a delegate and implement gestureRecognizerShouldBegin. Here, we look to see where the tap is. If it is within the bounds of a cell, we return false; otherwise we return true. We thus mediate between the cell tap and gesture recognizer tap.
Here is a possible implementation, demonstrated in a highly simplified form:
extension UIView {
func isDescendant(of whattype:UIView.Type) -> Bool {
var sup : UIView? = self.superview
while sup != nil {
if (whattype == type(of:sup!)) {
return true
}
sup = sup!.superview
}
return false
}
}
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let t = UITapGestureRecognizer(target: self, action: #selector(tap))
self.view.addGestureRecognizer(t)
t.delegate = self
}
#objc func tap(_:UIGestureRecognizer) {
print("tap")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("select")
}
func gestureRecognizerShouldBegin(_ gr: UIGestureRecognizer) -> Bool {
if let v = gr.view {
let loc = gr.location(in: v)
if let v2 = v.hitTest(loc, with: nil) {
return !v2.isDescendant(of: UICollectionViewCell.self)
}
}
return true
}
}
As you can see, we look to see whether the tap is inside a collection view cell; if it is, our gesture recognizer is prevented from recognizing, and the selection succeeds immediately.
Probably there is a UIGesture or another interactable thing underneath the collection view. You should DISABLE its ability to cancel touches in view in interface builder:
or in code:
myTapGestureRecognizer.cancelsTouchesInView = false
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 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")
}
}
I have a UICollectionView and each cell has a UITextView. I'm trying to make it possible to delete the entire cell when long pressing the text in a TextView and selecting Delete from the context menu.
I know I can override Delete by overriding
func delete(_ sender: Any?) {//I want to delete cell from here}
and
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(delete(_:))
{
return true
}
return super.canPerformAction(action, withSender: sender)
}
I just don't know how to find the current cell I am in.
In your func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell set indexPath.row as tag of your UITextView. Then in your func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) or UITextView long press you can access the tag of UITextView which helps to find the current cell.
You can access current cell using this code:
let indexPath = IndexPath(row: textview.tag, section: 0)
let yourCell = self.collectionView.cellForItem(at: indexPath)
I hope this will help you, Or you are experiencing any other problems, let me know :)
Give tag to textfield which should be equal to current indexPath while adding cell to collection view. When long press listener is triggered, check the tag value and based on that, do you operation.
Swift 3 Solution:
So some people were close, but everyone led me down the right path almost. The solution doesn't exactly answer my question, but it could be combined with a gesturerecognizer to answer the question specifically.
So my solution is based on how my code is formatted and may need to be tweaked for anyone else.
First:
From the UICollectionViewCell subclass, where my textview is implemented of course, I overrode the built in delete function. From there is created a variable that is instantiated with with the cells superview, which is a UICollectionView. This allows me to get the indexPath for the cell. From there I also create a variable that is instantiated with the UIView for where my UICollectionView is located. From there I created a function inside my UIView that takes in a IndexPath and deletes the cell.
Inside UICollectionViewCell:
override func delete(_ sender: Any?) {
let cv = self.superview as! UICollectionView
let indexPath = cv.indexPath(for: self)
let view = self.superview?.superview as! UIView
view.delCell(indexPath!)
}
Inside UIView:
func delCell(indexPath: IndexPath) {/*code for cell removal here*/}
func deleteSections(IndexSet)
Deletes the sections at the specified indexes.
func deleteItems(at: [IndexPath])
Deletes the items at the specified index paths