nlampi / SwiftGridView

Swift based data grid view.
MIT License
58 stars 15 forks source link

dataGridView.indexPathForItem(at:CGPoint) returns nil after scrolling #41

Open robcecil opened 6 years ago

robcecil commented 6 years ago

dataGridView.indexPathForItem(at:CGPoint) works fine until I scroll around. It seems it always returns nil after I scroll for a large range of values, like

for x in 0...400 {
            for y in 0...400 {
                let pt2 = CGPoint(x: x, y: y)
                let indexPath = self.dataGridView.indexPathForItem(at: pt2)
                print ("\(pt2) ==> \(indexPath)")
            }
        }
(0.0, 0.0) ==> nil
(0.0, 1.0) ==> nil
(0.0, 2.0) ==> nil
(0.0, 3.0) ==> nil
(0.0, 4.0) ==> nil
(0.0, 5.0) ==> nil
(0.0, 6.0) ==> nil
(0.0, 7.0) ==> nil
(0.0, 8.0) ==> nil
(0.0, 9.0) ==> nil
...
(400.0, 395.0) ==> nil
(400.0, 396.0) ==> nil
(400.0, 397.0) ==> nil
(400.0, 398.0) ==> nil
(400.0, 399.0) ==> nil
(400.0, 400.0) ==> nil
nlampi commented 6 years ago

@robcecil I am still looking into what can be done here. When scrolling a point may be either not on a row cell (e.g. header or footer). If the point is not visible (if a user scrolls beyond 400px in your example) then the layout for that area may not be calculated for performance reasons. In theory I could add in a check to manually calculate the layout in the region to determine the indexPath (if the point does correspond to a cell). I'm not sure if this would be very performant for high volume calling, but is that something which would be interesting for you?

robcecil commented 6 years ago

Ok, maybe if I describe my end goal that would be more helpful. I need to attach an Edit Menu (UIMenuController) that will show when the user double-taps a cell.

    @objc func wasDoubleTapped(_ sender: UITapGestureRecognizer) {
        print("was double tapped ")
        guard let senderView = sender.view,
            let superView = sender.view?.superview
            else { return }

        let pt = sender.location(in: senderView)
        if let indexPath = self.dataGridView.indexPathForItem(at: pt) {
            if let cell = self.dataGridView.cellForItem(at: indexPath) {
                cell.becomeFirstResponder()

                let drillData = UIMenuItem(title: "Drill Data", action: #selector(drillDataMenuTapped))
                let drillSource = UIMenuItem(title: "Drill Source", action: #selector(drillSourceMenuTapped))
                UIMenuController.shared.menuItems = [drillData, drillSource]
                UIMenuController.shared.setTargetRect(cell.frame, in: superView)
                UIMenuController.shared.setMenuVisible(true, animated: true)
            }
        }
    }

Unfortunately, the single-tap selection is getting in the way. What I mean is I need to invoke the Edit Menu, and keep the current selection stable. So, I have had to eschew didDeselectCellAtIndexPath and instead I have tried installing two gesture handlers - one for tap, the other for double tap:

        singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(GridViewController.wasSingleTapped(_:)))
        doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(GridViewController.wasDoubleTapped(_:)))

        singleTapGesture.require(toFail: doubleTapGesture)

        doubleTapGesture.numberOfTapsRequired = 2
        singleTapGesture.numberOfTapsRequired = 1

        singleTapGesture.delaysTouchesBegan = true

        self.dataGridView.addGestureRecognizer(doubleTapGesture)
        self.dataGridView.addGestureRecognizer(singleTapGesture)

Which brings me to my original question regarding dataGridView.indexPathForItem(at: CGPoint) always returns nil, so I have no way to get my handlers to work.

nlampi commented 6 years ago

I now see what was going wrong with your attempts. Since the collectionView is private, the gesture recognizer was not pulling the expected location/point. In the short term I have added a new method in v0.6.6 which allows you to pass a gesture recognizer to get the expected location. I will need to investigate gestures as a whole more to see what can be done to make this a bit easier to implement.

Use the following instead of "sender.location(in: senderView)" self.dataGridView.location(for: sender)

robcecil commented 6 years ago

Pretty close. Except the setTargetRect also needs a valid view as the second parameter. Once the view has scrolled, I cannot get it to work, unless I expose the underlying UICollectionView and use that.

UIMenuController.shared.setTargetRect(cell.frame, in: self.dataGridView.collectionView)
nlampi commented 6 years ago

Yeah, it doesn't look like there's too much of an elegant way around this short term so I also exposed the internal collectionView in v0.6.7. I'll continue looking into what can be done here.