Is there a method to check the UICollectionView item drag cancellation if the item wasn't moved?
If you use the standard UICollectionViewDragDelegate implementation, you can react to the cell state change using the func dragStateDidChange(_ dragState: UICollectionViewCellDragState)
of the cell class itself like:
override func dragStateDidChange(_ dragState: UICollectionViewCellDragState) {
switch dragState {
case .none:
//
case .lifting:
//
case .dragging:
//
}
}
Identify when UIView is dragged on top of a UICollectionViewCell
I don't know if there's a tidier way, but perhaps you can get the point of the tap and/or some points on the corners of the dragged UIView and then use CGRectContainsPoint to see if those points are within the rect of the collection view cell.
UICollectionView effective drag and drop
This might help
https://github.com/lxcid/LXReorderableCollectionViewFlowLayout
This is extends the UICollectionView
to allow each of the UICollectionViewCells
to be rearranged manually by the user with a long touch (aka touch-and-hold). The user can drag the Cell to any other position in the collection and the other cells will reorder automatically. Thanks go to lxcid for this.
UICollectionViewDropDelegate wrong destination index path in `dropSessionDidUpdate`
So, it's a bug of UIKit. The correct destination index path may be calculated as following:
func collectionView(
_ collectionView: UICollectionView,
dropSessionDidUpdate session: UIDropSession,
withDestinationIndexPath destinationIndexPath: IndexPath?)
-> UICollectionViewDropProposal {
// Calculating location in view
let location = session.location(in: collectionView)
var correctDestination: IndexPath?
// Calculate index inside performUsingPresentationValues
collectionView.performUsingPresentationValues {
correctDestination = collectionView.indexPathForItem(at: location)
}
guard let destination = correctDestination else {
return UICollectionViewDropProposal(
operation: .cancel, intent: .unspecified
)
}
// check destination
// ...
}
To fix this bug, first, I tried to use the combination of location(in:)
and indexPathForItem(at:)
. The resulting index path was equal to destinationIndexPath
provided by the delegate method. Why? My attention was drawn by UIDataSourceTranslating. It's a protocol allowing collection and table views to show placeholder cells for drag & drop without changing the actual data source. And when the drag & drop interaction ends that placeholders are easily removed. So, I made an assumption that
destinationIndexPath
is calculated with help ofindexPathForItem(at:)
- It ignores the placeholders created by UIDataSourceTranslating, which is a bug
Then I tried to wrap indexPathForItem(at:)
into performUsingPresentationValues(_:)
and the received index path was correct!
UIKit collection view, strange behavior (accessing/scrolling cells)
Let's see what your two properties do.
delaysContentTouches
: If the value of this property is true, the scroll view delays handling the touch-down gesture until it can determine if scrolling is the intent. If the value is false , the scroll view immediately calls touchesShouldBegin(_:with:in:). The default value is true.
canCancelContentTouches
: If the value of this property is true and a view in the content has begun tracking a finger touching it, and if the user drags the finger enough to initiate a scroll, the view receives a touchesCancelled(_:with:) message and the scroll view handles the touch as a scroll. If the value of this property is false, the scroll view does not scroll regardless of finger movement once the content view starts tracking.
First, you set delaysContentTouches to false. So the scrollview immediately calls the content view's touch handling methods, allowing it to handle the touch. Obviously, the scroll view won't start scrolling right away because of this, even if you drag.
Second, you also set canCancelContentTouches to false. But if the scroll view isn't allowed to "take over" touches that the content already handles (by cancelling them), it is never able to start scrolling later on either. So if your touch hits a content view, there is no possible way for the scroll view to start scrolling: it isn't allowed to scroll right away because it isn't allowed to delay the content touches, and it can't start scrolling later because it can't cancel the content touches.
I don't know what happens within your cells, not sure what code you put in there. However, you should probably allow your tableview to both delay touches (that means that your cell won't handle swipes that are cancelled immediately anyway because they were intended to be scroll gestures), and to cancel content touches (that means that when you touch down and don't release, you can still start a scroll gesture after a cell became highlighted).
UITableView Not Showing Reorder Controls
A few observations:
You are not going to get the reorder controls if you do not implement
tableView(_:moveRowAt:to:)
, e.g., assuming you had a model which was an array calledobjects
, you could do the following:func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let object = objects.remove(at: sourceIndexPath.row)
objects.insert(object, at: destinationIndexPath.row)
}The
trailingSwipeActionsConfigurationForRowAt
is probably not the right place to put a “reorder” command. Part of the reason is that once the table view is in edit mode and you tap on the ⛔️, the trailing actions show up, and “reorder” does not make sense in that context. E.g., here I am tapping on ⛔️ and I see the confusing actions.I would suggest only adding “delete” as the trailing action. That way, you (a) get only “delete” if you tap on ⛔️ in
isEditing
mode, but also (b) get the stand-alone swipe action, too.You cannot initiate
isEditing
from the trailing swipe actions (and, as discussed above, I do not think you want to, anyway). So, if you do not have “reorder” in the trailing swipe actions, you need some other method to enter edit mode. E.g., above, I added an “edit” button to the navigation bar that togglesisEditing
:@IBAction func didTapEdit(_ sender: Any) {
tableView.isEditing.toggle()
}Then, you can keep the swipe to delete functionality, but when you tap on edit button, you have the tap on ⛔️ to delete functionality (plus the handles for reordering because we added
tableView(_:moveRowAt:to:)
as outlined in step one, above):Another way to achieve reordering is to just allow drag and drop within the table view where you can long-press on a row and then drag it:
This is enabled by setting
dragInteractionEnabled
anddropDelegate
:class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
return formatter
}()
private var objects: [Foo] = ...
override func viewDidLoad() {
super.viewDidLoad()
...
tableView.dragInteractionEnabled = true
tableView.dropDelegate = self
}
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource { ... }
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "delete") { [weak self] action, view, completion in
self?.objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .middle)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
// This is used if table view is in `isEditing` mode and by `UITableViewDropDelegate`
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let object = objects.remove(at: sourceIndexPath.row)
objects.insert(object, at: destinationIndexPath.row)
}
}
// MARK: - UITableViewDropDelegate
extension ViewController: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
guard
session.items.count == 1, // Accept only one drag item ...
tableView.hasActiveDrag // ... from within this table view
else {
return UITableViewDropProposal(operation: .cancel)
}
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
guard let destinationIndexPath = coordinator.destinationIndexPath else { return }
for item in coordinator.items {
if let sourceIndexPath = item.sourceIndexPath {
DispatchQueue.main.async {
tableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
}
}
}
}
}Clearly, if you were going to enable drag from this app to others, you would add
UITableViewDragDelegate
conformance here, and make your model objects conform toNSItemProviderReading
andNSItemProviderWriting
. But the above should be sufficient for dragging and dropping to reorder within aUITableView
.
Related Topics
Access Class Property from Instance
Location Access Request in iOS 11
Easiest Way to Truncate Float to 2 Decimal Places
Swift Protocol Extension Implementing Another Protocol with Shared Associated Type
Uidatepicker Show Only Sunday's Date Only
Carthage Update Error: "Github API Request Failed: Bad Credentials"
Combined Watch Os and iOS Framework
How to Test Whether an Array's Type Is Optional
Swift Short Syntax of Execution
In Swift 3.1, Unsafemutablepointer.Initialize(From:) Is Deprecated
When How to Start Submitting Apps to The iOS App Store Written Using The Swift Programming Language
Is There a Method to Check The UIcollectionview Item Drag Cancellation If The Item Wasn't Moved
Why I Can Not Use Setvalue for Dictionary
Pfobject Unable to Be Cast to Custom Subclass
Sizing a UIpickerview Inside a UIalertview
Picker Not Working When Editmode Is Active
Swift Wkwebview: Can't Find Variable Error When Calling Method