Search code examples
iosswift3dtouch

Detecting UITouches while UIViewController is being shown as 3DTouch preview


Is it possible to detect touches and get the location of a touch from a UIViewController which is being currently used as previewingContext view controller for 3D Touch? (I want to change the image of within the preview controller when the touch moves from left to right)

I've tried both touchesBegan and touchesMoved none of them are fired.

class ThreeDTouchPreviewController: UIViewController {

func getLocationFromTouch(touches: Set<UITouch>) -> CGPoint?{
        guard let touch  = touches.first else { return nil }
        return touch.location(in: self.view)
    }
    //Not fired
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        let location = getLocationFromTouch(touches: touches)
        print("LOCATION", location)
    }
    //Not fired
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let location = getLocationFromTouch(touches: touches)
        print("LOCATION", location)
    }
}

Even tried adding a UIPanGesture.

Attempting to replicate FaceBook's 3D Touch feature where a user can move finger from left to right to change the current image being displayed. Video for context: https://streamable.com/ilnln


Solution

  • If you want to achive result same as in FaceBook's 3D Touch feature you need to create your own 3D Touch gesture class

        import UIKit.UIGestureRecognizerSubclass
    
    class ForceTouchGestureRecognizer: UIGestureRecognizer {
    
        var forceValue: CGFloat = 0
        var isForceTouch: Bool = false
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
            super.touchesBegan(touches, with: event)
            handleForceWithTouches(touches: touches)
            state = .began
            self.isForceTouch = false
        }
    
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
            super.touchesMoved(touches, with: event)
            handleForceWithTouches(touches: touches)
            if self.forceValue > 6.0 {
                state = .changed
                self.isForceTouch = true
            }
        }
    
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
            super.touchesEnded(touches, with: event)
            state = .ended
            handleForceWithTouches(touches: touches)
        }
    
        override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
            super.touchesCancelled(touches, with: event)
            state = .cancelled
            handleForceWithTouches(touches: touches)
        }
    
        func handleForceWithTouches(touches: Set<UITouch>) {
            if touches.count != 1 {
                state = .failed
                return
            }
    
            guard let touch = touches.first else {
                state = .failed
                return
            }
            forceValue = touch.force
        }
    }
    

    and now you can add this gesture in your ViewController in viewDidLoad method

    override func viewDidLoad() {
        super.viewDidLoad()
        let gesture = ForceTouchGestureRecognizer(target: self, action: #selector(imagePressed(sender:)))
        self.view.addGestureRecognizer(gesture)
    }
    

    Now you can manage your controller UI in Storyboard. Add cover view above UICollectionView and UIImageView in a center and connect it with IBOutlets in code.

    Now you can add handler methods for gesture

    func imagePressed(sender: ForceTouchGestureRecognizer) {

    let location = sender.location(in: self.view)
    
    guard let indexPath = collectionView?.indexPathForItem(
        at: location) else { return }
    

    let image = self.images[indexPath.row]

    switch sender.state {
    case .changed:
        if sender.isForceTouch {
            self.coverView?.isHidden = false
            self.selectedImageView?.isHidden = false
            self.selectedImageView?.image = image
        }
    
    case .ended:
        print("force: \(sender.forceValue)")
        if sender.isForceTouch {
            self.coverView?.isHidden = true
            self.selectedImageView?.isHidden = true
            self.selectedImageView?.image = nil
        } else {
            //TODO: handle selecting items of UICollectionView here,
            //you can refer to this SO question for more info: https://stackoverflow.com/questions/42372609/collectionview-didnt-call-didselectitematindexpath-when-superview-has-gesture
            print("Did select row at indexPath: \(indexPath)")
            self.collectionView?.selectItem(at: indexPath, animated: true, scrollPosition: .centeredVertically)
        }
    
    default: break
    }
    

    }

    From this point you need to customize your view to make it look in same way as Facebook do.

    Also I created small example project on GitHub https://github.com/ChernyshenkoTaras/3DTouchExample to demonstrate it