How can I end a touch/gesture with a long hold?

Viewed 69

I'd like to move a simple, small UIView around the screen and 'drop' it off at any location on the screen, but accurately. Simply lifting your finger from the screen does not have the desired effect as there is always some movement upon lifting your finger, resulting in the object not being in the required location. What I'm looking for is some way to count down a specified number of milliseconds AFTER holding/pausing at the desired location and then have some mechanism ENDING my touches/gesture so the object is placed EXACTLY where I want it.

I've been reasonably successful with touchesBegan/Moved/Ended, but even though I called the touchesEnded method the touches never really end as I can still drag around the object on the screen and relocate it - not what I want.

import UIKit

class ViewController: UIViewController {

let greenDot : UIView = {
    let greenDot = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
    greenDot.backgroundColor = .green
    greenDot.layer.cornerRadius = greenDot.bounds.height / 2
    return greenDot
}()

var timer : Timer?
var lastTouch = Set<UITouch>()
var lastTouchEvent: UIEvent?

override func viewDidLoad() {
    super.viewDidLoad()
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    print(touch.location(in: view))
    greenDot.center = touch.location(in: view)
    view.addSubview(greenDot)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    print(touch.location(in: view))
    if timer != nil { timer?.invalidate() }
    greenDot.center = touch.location(in: view)
    view.addSubview(greenDot)
    lastTouch = touches
    lastTouchEvent = event
    timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(stopTouches), userInfo: nil, repeats: false)
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    timer?.invalidate()
    print("Touches Ended @: \(touch.location(in: view))")
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    print("Cancelling touch")
}

@objc func stopTouches() {
    touchesEnded(lastTouch, with: lastTouchEvent)
//        touchesCancelled(lastTouch, with: lastTouchEvent)
//        view.resignFirstResponder()
    }
}

With UIGestures I have tried with the UILongPressGestureRecognizer, but I don't know how to 'end' with a long-press. Do I use 2 long-presses in sequence - one to start the movement and another to end the movement? I like the longPress since it is continuous and I can thus pan with it.

import UIKit

class ViewController: UIViewController {

let greenDot : UIView = {
    let dot = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
    dot.backgroundColor = .green
    dot.layer.cornerRadius = dot.frame.width/2
    return dot
}()

override func viewDidLoad() {
    super.viewDidLoad()
    
    let longPress1 = UILongPressGestureRecognizer(target: self, action: #selector(press1))
    longPress1.minimumPressDuration = 0.2
    view.addGestureRecognizer(longPress1)
}

@objc func press1(_ sender: UILongPressGestureRecognizer) {
    
    let newLocation = sender.location(in: view)
    greenDot.backgroundColor = .green
    greenDot.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
    greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y - 40)
        
    if sender.state == .ended {
        greenDot.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
        greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y - 40)
        greenDot.backgroundColor = .purple
    }
    view.addSubview(greenDot)
}

}

So in conclusion: I'm looking for a method to place an object, which I am moving around on the screen with my finger, accurately.

Any help in this regard will be greatly appreciated.

1 Answers

Intriguing how often you end up answering your own question after placing it before others.

So after many days of fumbling around I stumbled across this pointer from Apple: https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/implementing_a_custom_gesture_recognizer/implementing_a_discrete_gesture_recognizer

In short, I have the basics of what I was looking for by creating a custom Gesture Recognizer. Thanks Apple.

So for anyone else who might be interested, following is the 'basic' code for grabbing an object, moving it around and letting a timer release it for you so that the drop is as accurate as needed. Releasing before the timer expires also works, but then some shifting might/will occur during release. Once the timer has fired and your finger is still on the screen, you will be unable to accidentally drag the object out of place.
Obviously there is still a lot of work needed before implementing in an app to mitigate errors. 'Small steps'

import UIKit
import UIKit.UIGestureRecognizerSubclass

class ObjectDropGestureRecognizer: UIGestureRecognizer {

var trackedTouch    : UITouch?      = nil
var intervalTime    : Double        = 1.0
var dropTimer       : Timer?
var counter         : Int           = 0

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
    super.touchesBegan(touches, with: event)
    print(touches.count)
    if touches.count != 1 {
        self.state = .failed
    }
    if self.state == .possible {
    self.state = .began
        print("began...")
    }
    if self.trackedTouch == nil {
        self.trackedTouch = touches.first
    } else {
        for touch in touches {
            if touch != self.trackedTouch {
                self.ignore(touch, for: event)
            }
        }
    }
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
    super.touchesMoved(touches, with: event)
    guard let newTouch = touches.first else { return }
    self.state = .changed
    counter += 1
    print("Changed...\(counter)")
    if self.dropTimer != nil { dropTimer?.invalidate() }
    dropTimer = Timer.scheduledTimer(withTimeInterval: intervalTime, repeats: false) { timer in
        print("Timer fired")
        print(newTouch.location(in: self.view))
        self.state = .ended
    }
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
    super.touchesEnded(touches, with: event)
    if self.dropTimer != nil {
        self.dropTimer?.invalidate()
        self.state = .ended
    }
    self.state = .recognized
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
    super.touchesCancelled(touches, with: event)
    self.trackedTouch = nil
    self.state = .cancelled
}

override func reset() {
    super.reset()
    self.trackedTouch = nil
    self.counter = 0
}
}

class ViewController: UIViewController {

let greenDot : UIView = {
    let dot = UIView(frame: CGRect(x: 50, y: 200, width: 20, height: 20))
    dot.backgroundColor = .green
    dot.layer.cornerRadius = dot.frame.width/2
    return dot
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(greenDot)
    
    let objectMoveGesture = ObjectDropGestureRecognizer(target: self, action: #selector(tapAction(_:)))
    objectMoveGesture.intervalTime = 0.8
    view.addGestureRecognizer(objectMoveGesture)
}

@objc func tapAction(_ touch: ObjectDropGestureRecognizer){
    
    let newLocation = touch.location(in: view)
        greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y)
}
}
    
Related