I'm trying to implement two collectionViews with drag and drop cells between them. But i faced strange behaviour with reordering cells inside collectionView. Here is minimal code that replicate that behaviour.
Provided code works as expected but then i uncomment collectionView.dropDelegate = self it doesn't work anymore. I tried to find which method of UICollectionViewDropDelegate is called but none of them is called.
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
print("in drop")
}
#IBOutlet weak var collectionView: UICollectionView!
var items = [UIColor.red, UIColor.green, UIColor.blue]
override func viewDidLoad() {
super.viewDidLoad()
// collectionView.dropDelegate = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.contentView.layer.backgroundColor = items[indexPath.item].cgColor
return cell
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let temp = items.remove(at: sourceIndexPath.item)
items.insert(temp, at: destinationIndexPath.item)
}
#IBAction func handleLongGesture(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
guard let selectedIndexPath = self.collectionView.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
break
}
self.collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
self.collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case .ended:
self.collectionView.endInteractiveMovement()
default:
self.collectionView.cancelInteractiveMovement()
}
}
}
example without dragDelegate
example with dragDelegate
So this it happens and what i should do to get normal behaviour with dragDelegate?
It's a bit ugly, but I had this problem and the simplest thing I could find that works is to disable the dropDelegate when your gesture recognizer is invoked.
#IBAction func handleLongGesture(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
guard let selectedIndexPath = self.collectionView.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
break
}
self.collectionView.dropDelegate = nil // Drop delegate behaves VERY badly with beginInteractiveMovementForItem
self.collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
self.collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case .ended:
self.collectionView.endInteractiveMovement()
self.collectionView.dropDelegate = self
default:
self.collectionView.cancelInteractiveMovement()
self.collectionView.dropDelegate = self
}
}
Related
I'm working on a 3rd party framework for swift so i cannot use the delegate methods of UICollectionViewDelegate myself but I do need them for some custom logic.
Tried multiple approaches to make it work, including method swizzling but in the end I felt like it was too hacky for what i'm doing.
Now i'm subclassing UICollectionView and setting the delegate to an internal (my) delegate.
This works well except for when the UIViewController hasn't implemented the method.
right now my code looks like this:
fileprivate class UICollectionViewDelegateInternal: NSObject, UICollectionViewDelegate {
var userDelegate: UICollectionViewDelegate?
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || userDelegate?.responds(to: aSelector) == true
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if userDelegate?.responds(to: aSelector) == true {
return userDelegate
}
return super.forwardingTarget(for: aSelector)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let collection = collectionView as! CustomCollectionView
collection.didEnd(item: indexPath.item)
userDelegate?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
}
}
class CustomCollectionView: UICollectionView {
private let internalDelegate: UICollectionViewDelegateInternal = UICollectionViewDelegateInternal()
required init?(coder: NSCoder) {
super.init(coder: coder)
super.delegate = internalDelegate
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
super.delegate = internalDelegate
}
func didEnd(item: Int) {
print("internal - didEndDisplaying: \(item)")
}
override var delegate: UICollectionViewDelegate? {
get {
return internalDelegate.userDelegate
}
set {
self.internalDelegate.userDelegate = newValue
super.delegate = nil
super.delegate = self.internalDelegate
}
}
}
In the ViewController I just have a simple set up with the delegate method for didEndDisplaying not implemented
Is it possible to listen to didEndDisplaying without the ViewController having it implemented?
Edit 1:
Here's the code of the ViewController to make it a little more clear what i'm doing
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CustomCollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
1000
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .blue
return cell
}
// func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// print("view controller - did end displaying: \(indexPath.item)")
// }
}
the didEndDisplaying of CustomCollectionView is only triggered when i uncomment the didEndDisplaying method in the ViewController.
what i'm looking for is to have the didEndDisplaying of CustomCollectionView also triggered if the didEndDisplaying method in the ViewController is NOT implemented.
hope it's a little more clear now
Edit 2:
Figured out that the code above had some mistakes which made the reproduction not work as I intended. updated the code above.
also made a github page to make it easier to reproduce here:
https://github.com/mees-vdb/InternalCollectionView-Delegate
I did a little reading-up on this approach, and it seems like it should work - but, obviously, it doesn't.
Played around a little bit, and this might be a solution for you.
I made very few changes to your existing code (grabbed it from GitHub - if you want to add me as a Collaborator I can push a new branch [same DonMag ID on GitHub]).
First, I implemented didSelectItemAt to make it a little easier to debug (only one event at a time).
ViewController class
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CustomCollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
1000
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .blue
return cell
}
// DonMag - comment / un-comment these methods
// to see the difference
//func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// print("view controller - did end displaying: \(indexPath.item)")
//}
//func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// print("view controller - didSelectItemAt", indexPath)
//}
}
UICollectionViewDelegateInternal class
fileprivate class UICollectionViewDelegateInternal: NSObject, UICollectionViewDelegate {
var userDelegate: UICollectionViewDelegate?
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || userDelegate?.responds(to: aSelector) == true
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if userDelegate?.responds(to: aSelector) == true {
return userDelegate
}
return super.forwardingTarget(for: aSelector)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let collection = collectionView as! CustomCollectionView
collection.didEnd(item: indexPath.item)
userDelegate?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let collection = collectionView as! CustomCollectionView
collection.didSel(p: indexPath)
userDelegate?.collectionView?(collectionView, didSelectItemAt: indexPath)
}
}
CustomCollectionView class
// DonMag - conform to UICollectionViewDelegate
class CustomCollectionView: UICollectionView, UICollectionViewDelegate {
private let internalDelegate: UICollectionViewDelegateInternal = UICollectionViewDelegateInternal()
required init?(coder: NSCoder) {
super.init(coder: coder)
super.delegate = internalDelegate
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
super.delegate = internalDelegate
}
func didEnd(item: Int) {
print("internal - didEndDisplaying: \(item)")
}
func didSel(p: IndexPath) {
print("internal - didSelectItemAt", p)
}
// DonMag - these will NEVER be called,
// whether or not they're implemented in
// UICollectionViewDelegateInternal and/or ViewController
// but, when implemented here,
// it allows (enables?) them to be called in UICollectionViewDelegateInternal
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
print("CustomCollectionView - didEndDisplaying", indexPath)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("CustomCollectionView - didSelectItemAt", indexPath)
}
override var delegate: UICollectionViewDelegate? {
get {
// DonMag - return self instead of internalDelegate.userDelegate
return self
//return internalDelegate.userDelegate
}
set {
self.internalDelegate.userDelegate = newValue
super.delegate = nil
super.delegate = self.internalDelegate
}
}
}
I want to detect a tap on imageview in uicollectionviewcell inside uitableviewcell
I'm using an api response to build a data in my tableview
I have this API response:
{"status":1,"data":{"blocks":[{"name":"CustomBlock","description":"CustomDescription","itemsType":"game","items":[510234,78188,15719,37630]}], "items":[{"id:"1", name: "testgame"}]}
BlocksViewController.swift
class BlocksViewController: UIViewController, UITableViewDataSource, UICollectionViewDataSource, UICollectionViewDelegate, UITableViewDelegate {
var blocks = [Block]() // I'm getting blocks in this controller
var items : BlockItem! // and items
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return blocks[collectionView.tag].items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GameCollectionCell", for: indexPath) as? GameCollectionCell else { return
UICollectionViewCell() }
if let found = items.game.first(where: {$0.id == String(blocks[collectionView.tag].items[indexPath.row])}) {
cell.gameName.text = found.name
cell.gameImage.kf.indicatorType = .activity
let processor = DownsamplingImageProcessor(size: CGSize(width: 225, height: 300))
cell.gameImage.kf.setImage(
with: URL(string: found.background_image ?? ""),
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(0.2)),
.cacheOriginalImage
])
}
else {
cell.gameName.text = ""
cell.gameImage.image = nil
}
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return blocks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "BlockCell") as? BlockCell else { return UITableViewCell() }
cell.blockName.text = blocks[indexPath.row].name
cell.blockDescription.text = blocks[indexPath.row].description
cell.setScrollPosition(x: offsets[indexPath] ?? 0)
cell.gameCollectionCell.delegate = self
cell.gameCollectionCell.dataSource = self
cell.gameCollectionCell.tag = indexPath.row
cell.gameCollectionCell.reloadData()
return cell
}
I'm getting blocks and items in this controller. Now i want to detect a tap using LongTapGestureRecognizer on image in gamecollectionCell(UIcollectionViewCell inside BlockCell(TableviewCell). How can i do this? Or maybe any advice how to improve logic here?
Okay, i've added gesture recognizer like this in cellForItemAt :
cell.addGestureRecognizer(UILongPressGestureRecognizer.init(target: self, action: #selector(addGamePopUp)))
Then i need to animate uiimageview on long tap.
var selectedGameCell : GameCollectionCell?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedGameCell = collectionView.dequeueReusableCell(withReuseIdentifier: "GameCollectionCell", for: indexPath) as? GameCollectionCell
}
And
#IBAction func addGamePopUp(_ sender: UILongPressGestureRecognizer) {
if (sender.state == UIGestureRecognizer.State.began){
UIView.animate(withDuration: 0.3, animations: {
self.selectedGameCell?.gameImage.transform = CGAffineTransform(scaleX: 0.95,y: 0.95);
}) { (Bool) in
UIView.animate(withDuration: 0.3, animations: {
self.selectedGameCell?.gameImage.transform = CGAffineTransform(scaleX: 1,y: 1);
});
}
}
}
But it still doesn't work. Did i miss something?
If you want to use longTapGestureRecognizer, just add one to the cell in your cellForItemAtIndexPath method of your collectionView, like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SubjectCellId", for: indexPath) as? SubjectCell {
cell.addGestureRecognizer(UILongPressGestureRecognizer.init(target: self, action: #selector(someMethod)))
return cell
}
return UICollectionViewCell()
}
You can use following delegate method of uicollectionview to detect tap on collection view cell.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath){
print("cell tapped")
}
For Adding Long Press Gesture Add Following Code in Cell For item at indexpath method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell : GameCollectionCell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! GameCollectionCell
cell.backgroundColor = model[collectionView.tag][indexPath.item]
let lpgr = UILongPressGestureRecognizer(target: self, action: #selector(addGamePopUp(_:)))
cell.addGestureRecognizer(lpgr)
return cell
}
#IBAction func addGamePopUp(_ sender: UILongPressGestureRecognizer){
print("add game popup")
if (sender.state == UIGestureRecognizer.State.began){
UIView.animate(withDuration: 0.3, animations: {
self.selectedGameCell?.gameImage?.transform = CGAffineTransform(scaleX: 0.95,y: 0.95);
}) { (Bool) in
UIView.animate(withDuration: 0.3, animations: {
self.selectedGameCell?.gameImage?.transform = CGAffineTransform(scaleX: 1,y: 1);
});
}
}
}
You can use touchesBegan method inside tableview cell and from the touch location get the collection view cell object inside it.
NOTE: When you implement this method the didSelectRow method would not be called for the TableViewCell.
extension TableViewCell {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let point = touch.location(in: self)
if let path = collectionView.indexPathForItem(at: point) {
//Get cell of collection view from this index path
}
}
}
}
In order to get things working, I'd recommend changing your function from
#IBAction func addGamePopUp(_ sender: UILongPressGestureRecognizer) {
to
#objc func addGamePopUp() {
And when you're adding the longTapGestureRecognizer to your collectionViewCell, you'll have it trigger that method by changing the line to:
cell.addGestureRecognizer(UILongPressGestureRecognizer.init(target: self, action: #selector(addGamePopUp)))
Let me know if that works!
(psst: also take out this if check in your addGamePopupMethod if you're going this route)
if (sender.state == UIGestureRecognizer.State.began){
I have a collectionview and will support the drag and drop functionality of iOS 11. One requirement is that cells need to be removed by dragging them on a garbage bin on the bottom of the view. Is there a another possibility then using a second collectionview that holds that delete symobol?
Unfortunately a UIView can't be a UICollectionViewDropDelegate.
Best solution so far is to put an invisible collectionview above the delete icon. Here is my code:
import UIKit
class DragDropViewController: UIViewController
{
private var items1 = [String]()
//MARK: Outlets
#IBOutlet weak var collectionView1: UICollectionView!
#IBOutlet weak var collectionView2: UICollectionView!
#IBOutlet weak var trashImage: UIImageView!
private func createData(){
for index in 1...130{
items1.append("\(index)")
}
}
private func indexForIdentifier(identifier: String)->Int?{
return items1.firstIndex(of: identifier)
}
//MARK: View Lifecycle Methods
override func viewDidLoad()
{
super.viewDidLoad()
createData()
trashImage.alpha = 0
trashImage.layer.cornerRadius = 30
self.collectionView1.dragInteractionEnabled = true
self.collectionView1.dragDelegate = self
self.collectionView1.dropDelegate = self
self.collectionView2.dropDelegate = self
}
//MARK: Private Methods
/// This method moves a cell from source indexPath to destination indexPath within the same collection view. It works for only 1 item. If multiple items selected, no reordering happens.
///
/// - Parameters:
/// - coordinator: coordinator obtained from performDropWith: UICollectionViewDropDelegate method
/// - destinationIndexPath: indexpath of the collection view where the user drops the element
/// - collectionView: collectionView in which reordering needs to be done.
private func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView)
{
let items = coordinator.items
if items.count == 1, let item = items.first, let sourceIndexPath = item.sourceIndexPath
{
var dIndexPath = destinationIndexPath
if dIndexPath.row >= collectionView.numberOfItems(inSection: 0)
{
dIndexPath.row = collectionView.numberOfItems(inSection: 0) - 1
}
collectionView.performBatchUpdates({
self.items1.remove(at: sourceIndexPath.row)
self.items1.insert(item.dragItem.localObject as! String, at: dIndexPath.row)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [dIndexPath])
})
coordinator.drop(items.first!.dragItem, toItemAt: dIndexPath)
}
}
/// This method copies a cell from source indexPath in 1st collection view to destination indexPath in 2nd collection view. It works for multiple items.
///
/// - Parameters:
/// - coordinator: coordinator obtained from performDropWith: UICollectionViewDropDelegate method
/// - destinationIndexPath: indexpath of the collection view where the user drops the element
/// - collectionView: collectionView in which reordering needs to be done.
private func removeItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView)
{
collectionView.performBatchUpdates({
for item in coordinator.items
{
guard let identifier = item.dragItem.localObject as? String else {
return
}
if let index = indexForIdentifier(identifier: identifier){
let indexPath = IndexPath(row: index, section: 0)
items1.remove(at: index)
collectionView1.deleteItems(at: [indexPath])
}
}
})
}
}
// MARK: - UICollectionViewDataSource Methods
extension DragDropViewController : UICollectionViewDataSource
{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return collectionView == self.collectionView1 ? self.items1.count : 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell1", for: indexPath) as! DragDropCollectionViewCell
cell.customLabel.text = self.items1[indexPath.row].capitalized
return cell
}
}
// MARK: - UICollectionViewDragDelegate Methods
extension DragDropViewController : UICollectionViewDragDelegate
{
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]
{
let item = self.items1[indexPath.row]
let itemProvider = NSItemProvider(object: item as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem]
{
let item = self.items1[indexPath.row]
let itemProvider = NSItemProvider(object: item as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
}
// MARK: - UICollectionViewDropDelegate Methods
extension DragDropViewController : UICollectionViewDropDelegate
{
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool
{
return session.canLoadObjects(ofClass: NSString.self)
}
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal
{
if collectionView === self.collectionView1
{
return collectionView.hasActiveDrag ?
UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) :
UICollectionViewDropProposal(operation: .forbidden)
}
else
{
if collectionView.hasActiveDrag{
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
for item in session.items
{
guard let identifier = item.localObject as? String else {
return UICollectionViewDropProposal(operation: .forbidden)
}
//not every cell is allowed to be deleted
if Int(identifier)! % 3 == 0{
return UICollectionViewDropProposal(operation: .forbidden)
}
}
trashImage.backgroundColor = UIColor.red.withAlphaComponent(0.4)
return UICollectionViewDropProposal(operation: .move, intent: .unspecified)
}
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator)
{
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath
{
destinationIndexPath = indexPath
}
else
{
// Get last index path of table view.
let section = collectionView.numberOfSections - 1
let row = collectionView.numberOfItems(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
if coordinator.proposal.operation == .move{
if coordinator.proposal.intent == .insertAtDestinationIndexPath{
self.reorderItems(coordinator: coordinator, destinationIndexPath:destinationIndexPath, collectionView: collectionView)
}
else{
self.removeItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
}
}
}
func collectionView(_ collectionView: UICollectionView, dropSessionDidExit session: UIDropSession) {
trashImage.backgroundColor = UIColor.clear
}
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession) {
trashImage.backgroundColor = UIColor.clear
}
}
I have a UIViewController with a UICollectionViewController nested inside. The collection view controller also has the moveItemAt method implemented, since I want the cells to be reorderable. So the cells have a UILongPressGestureRecognizer attached. However, long press on the cells aren't happening. I can't seem to figure out if the nested controller is causing the gesture to be ignored. Maybe the parent controller is capturing the long press but that wouldn't make sense since AFAIK, gestures go up the view hierarchy, not the other way around.
For some context, I've used this method to nest my controllers
func add(_ child: UIViewController) {
addChildViewController(child)
child.view.frame = view.bounds
view.addSubview(child.view)
child.didMove(toParentViewController: self)
}
I think this is a better approach...
Assuming you have a UICollectionViewController in your storyboard, and you've assigned the cell prototype to DragMeCell and set its Identifier to "DragMeCell" (and added a label connected to the IBOutlet), this should run and allow you to long-press drag-drop to reorder.
//
// DragReorderCollectionViewController.swift
// SW4Temp
//
// Created by Don Mag on 7/25/18.
//
import UIKit
private let reuseIdentifier = "DragMeCell"
class DragMeCell: UICollectionViewCell {
#IBOutlet var theLabel: UILabel!
}
class DragReorderCollectionViewController: UICollectionViewController {
fileprivate var longPressGesture: UILongPressGestureRecognizer!
fileprivate var dataArray = Array(0 ..< 25)
override func viewDidLoad() {
super.viewDidLoad()
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongGesture(gesture:)))
if let cv = self.collectionView {
cv.addGestureRecognizer(longPressGesture)
}
}
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
if let cv = self.collectionView,
let gestureView = gesture.view {
switch(gesture.state) {
case .began:
guard let selectedIndexPath = cv.indexPathForItem(at: gesture.location(in: cv)) else {
break
}
cv.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
cv.updateInteractiveMovementTargetPosition(gesture.location(in: gestureView))
case .ended:
cv.endInteractiveMovement()
default:
cv.cancelInteractiveMovement()
}
}
}
// MARK: UICollectionViewDelegate
override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let i = dataArray[sourceIndexPath.item]
dataArray.remove(at: sourceIndexPath.item)
dataArray.insert(i, at: destinationIndexPath.item)
}
// MARK: UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArray.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! DragMeCell
// Configure the cell
cell.theLabel.text = "\(dataArray[indexPath.item])"
return cell
}
}
Then you can add it as a child view controller, using the code snippet you posted in your question.
I'm able to get the cells to move around but it's buggy. When you move them around the pictures change and they don't stay where you move them. When you scroll down and scroll back, they move back.
//
// CollectionViewController.swift
// 1.7 Task: Displaying Sets of Data: Collection View
//
// Created by Eric Andersen on 3/26/18.
// Copyright © 2018 Eric Andersen. All rights reserved.
//
import UIKit
class CollectionViewController: UICollectionViewController {
var batmanDataItems = [DataItem]()
var jokerDataItems = [DataItem]()
var allItems = [[DataItem]]()
var longPressGesture: UILongPressGestureRecognizer!
override func viewDidLoad() {
for i in 1...29 {
if i > 0 {
batmanDataItems.append(DataItem(title: "Title #\(i)", kind: Kind.Batman, imageName: "bat\(i).jpg"))
} else {
batmanDataItems.append(DataItem(title: "Title #0\(i)", kind: Kind.Batman, imageName: "bat0\(i).jpg"))
}
}
for i in 1...8 {
if i > 0 {
jokerDataItems.append(DataItem(title: "Another Title #\(i)", kind: Kind.Joker, imageName: "jok\(i).jpg"))
} else {
jokerDataItems.append(DataItem(title: "Another Title #0\(i)", kind: Kind.Joker, imageName: "jok0\(i).jpg"))
}
}
allItems.append(batmanDataItems)
allItems.append(jokerDataItems)
super.viewDidLoad()
let width = collectionView!.frame.width / 3
let layout = collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = CGSize(width: width, height: width)
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongGesture(gesture:)))
collectionView?.addGestureRecognizer(longPressGesture)
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Register cell classes
// Do any additional setup after loading the view.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
// MARK: UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return allItems[section].count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! DataItemCell
let dataItem = allItems[indexPath.section][indexPath.item]
cell.dataItem = dataItem
return cell
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let sectionHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "SectionHeader", for: indexPath) as! DataItemHeader
var title = ""
if let kind = Kind(rawValue: indexPath.section) {
title = kind.description()
}
sectionHeader.title = title
return sectionHeader
}
override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// print("Starting Index: \(sourceIndexPath.item)")
// print("Ending Index: \(destinationIndexPath.item)")
}
// MARK: UICollectionViewDelegate
// Uncomment this method to specify if the specified item should be highlighted during tracking
override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
return true
}
// Uncomment this method to specify if the specified item should be selected
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
allItems[indexPath.section].remove(at: indexPath.row)
self.collectionView?.performBatchUpdates({
self.collectionView?.deleteItems(at: [indexPath])
}) { (finished) in
self.collectionView?.reloadItems(at: (self.collectionView?.indexPathsForVisibleItems)!)
}
}
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case .began:
guard let selectedIndexPath = collectionView?.indexPathForItem(at: gesture.location(in: collectionView)) else {
break
}
collectionView?.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
collectionView?.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case .ended:
collectionView?.endInteractiveMovement()
default:
collectionView?.cancelInteractiveMovement()
}
}
}
enter image description here
I'm new. I know this is easy but I'm still getting the hang of this. Thanks for your patience!
Moving the row only updates the screen, but it doesn't change your model (the array that supplies the data to your collectionView). Then when cells go off screen and back on, they are loaded from your array which hasn't changed, which is why the cells go back to where they were.
You need to override func collectionView(_:moveItemAt:to:) and update your data array to reflect the row that was moved.
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// Remove the source item from the array and store it in item
let item = allItems[sourceIndexPath.section].remove(at: sourceIndexPath.item)
// insert the item into the destination location
allItems[destinationIndexPath.section].insert(item, at: destinationIndexPath.item)
}