Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to accelerate the identification of a single tap over a double tap?

I have a UITableView with row where I added single tap and double tap gestures:

let doubleTap = UITapGestureRecognizer(target: self, action: "doubleTap:")
doubleTap.numberOfTapsRequired = 2
doubleTap.numberOfTouchesRequired = 1
        
let singleTap = UITapGestureRecognizer(target: self, action: "singleTap:")
singleTap.numberOfTapsRequired = 1
singleTap.numberOfTouchesRequired = 1
singleTap.requireGestureRecognizerToFail(doubleTap)

tableView.addGestureRecognizer(doubleTap)
tableView.addGestureRecognizer(singleTap)

Is there a way to reduce the time between when the first tap is made and when the gesture recognizer realize that it is a single tap and not a double tap?

I'm asking this because when I do a single tap, the new viewController appear quite late, giving a feeling that the app lags.

like image 215
Nico Avatar asked Apr 04 '15 20:04

Nico


5 Answers

I found the answer on this link

The swift version:

class UIShortTapGestureRecognizer: UITapGestureRecognizer {
    let tapMaxDelay: Double = 0.3

    override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!) {
        super.touchesBegan(touches, withEvent: event)
        delay(tapMaxDelay) {
            // Enough time has passed and the gesture was not recognized -> It has failed.
            if  self.state != UIGestureRecognizerState.Ended {
                self.state = UIGestureRecognizerState.Failed
            }
        }
    }
}

With delay(delay: Double, closure:()->()):

class func delay(delay:Double, closure:()->()) {
        dispatch_after(dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), closure)
    }
like image 99
Nico Avatar answered Nov 17 '22 18:11

Nico


Inspired by Howard Yang's implementation, Swift 5.1 using DispatchWorkItem:

public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
    var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
    public var timeout: TimeInterval = 0.3 {
        didSet {
            self.targetDelegate.timeout = timeout
        }
    }

    public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
        targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
        super.init(target: targetDelegate, action: #selector(targetDelegate.recognizerAction(recognizer:)))
    }
}

class SingleDoubleTapGestureRecognizerDelegate: NSObject {
    weak var target: AnyObject?
    var singleAction: Selector
    var doubleAction: Selector
    var timeout: TimeInterval = 0.3
    var tapCount = 0
    var workItem: DispatchWorkItem?

    init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
        self.target = target
        self.singleAction = singleAction
        self.doubleAction = doubleAction
    }

    @objc func recognizerAction(recognizer: UITapGestureRecognizer) {
        tapCount += 1
        if tapCount == 1 {
            workItem = DispatchWorkItem { [weak self] in
                guard let weakSelf = self else { return }
                weakSelf.target?.performSelector(onMainThread: weakSelf.singleAction, with: recognizer, waitUntilDone: false)
                weakSelf.tapCount = 0
            }
            DispatchQueue.main.asyncAfter(
                deadline: .now() + timeout,
                execute: workItem!
            )
        } else {
            workItem?.cancel()
            DispatchQueue.main.async { [weak self] in
                guard let weakSelf = self else { return }
                weakSelf.target?.performSelector(onMainThread: weakSelf.doubleAction, with: recognizer, waitUntilDone: false)
                weakSelf.tapCount = 0
            }
        }
    }
}

Usage:

let singleDoubleTapRecognizer = SingleDoubleTapGestureRecognizer(
    target: self,
    singleAction: #selector(handleTapGesture),
    doubleAction: #selector(handleDoubleTapGesture)
)
like image 43
Astri Avatar answered Oct 20 '22 10:10

Astri


Full Implementation of Markus's Swift 3 version of eladleb's original solution.


Create subclass file UIShortTapGestureRecogninzer

import UIKit
import UIKit.UIGestureRecognizerSubclass 

class UIShortTapGestureRecognizer: UITapGestureRecognizer {
    let tapMaxDelay: Double = 0.3 //anything below 0.3 may cause doubleTap to be inaccessible by many users

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)

        DispatchQueue.main.asyncAfter(deadline: .now() + tapMaxDelay) { [weak self] in
            if self?.state != UIGestureRecognizerState.recognized {
                self?.state = UIGestureRecognizerState.failed
            }
        }
    }
}

Note: When adding UIGestureRecognizer only doubleTap needs to be of type UIShortTapGestureRecognizer & singleTap.require(toFail: doubleTap) is required.

func addBoth (views: UIView, selectorSingle: Selector, selectorDouble: Selector) {
    let doubleTap:UIShortTapGestureRecognizer = UIShortTapGestureRecognizer(target: self, action: selectorDouble)
    doubleTap.numberOfTapsRequired = 2
    views.addGestureRecognizer(doubleTap)

    let singleTap:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: selectorSingle)
    singleTap.numberOfTapsRequired = 1
    singleTap.require(toFail: doubleTap)
    views.addGestureRecognizer(singleTap)
}
like image 5
Chameleon Avatar answered Nov 17 '22 17:11

Chameleon


Swift 5 implementation of Nico's accepted answer.

class UIShortTapGestureRecognizer: UITapGestureRecognizer {
    var maximumTapLength: Double = 0.3

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)
        delay(delay: maximumTapLength) {
            // Enough time has passed and the gesture was not recognized -> It has failed.
            if  self.state != .ended {
                self.state = .failed
            }
        }
    }

    func delay(delay:Double, closure:@escaping ()->()) {
        DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: closure)
    }
}
like image 2
Randall Avatar answered Nov 17 '22 17:11

Randall


for future, full implementation by Howard Yang, here's links: https://github.com/oney/SingleDoubleTapGestureRecognizer

 let tap = SingleDoubleTapGestureRecognizer(target: self, singleAction: Selector("singleTap"), doubleAction: Selector("doubleTap"))
        tap.duration = 0.8
        view.addGestureRecognizer(tap)

https://github.com/oney/SingleDoubleTapGestureRecognizer/blob/master/Pod/Classes/SingleDoubleTapGestureRecognizer.swift

//
//  SingleDoubleTapGestureRecognizer.swift
//  SingleDoubleTapGestureRecognizer
//
//  Created by Howard Yang on 08/22/2015.
//  Copyright (c) 2015 Howard Yang. All rights reserved.
//

import UIKit

public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
    var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
    public var duration: CFTimeInterval = 0.3 {
        didSet {
            self.targetDelegate.duration = duration
        }
    }
    public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
        targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
        super.init(target: targetDelegate, action: Selector("fakeAction:"))
        numberOfTapsRequired = 1
    }
}
class SingleDoubleTapGestureRecognizerDelegate: NSObject {
    var target: AnyObject
    var singleAction: Selector
    var doubleAction: Selector
    var duration: CFTimeInterval = 0.3
    var tapCount = 0

    init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
        self.target = target
        self.singleAction = singleAction
        self.doubleAction = doubleAction
    }

    func fakeAction(g: UITapGestureRecognizer) {
        tapCount = tapCount + 1
        if tapCount == 1 {
            delayHelper(duration, task: {
                if self.tapCount == 1 {
                    NSThread.detachNewThreadSelector(self.singleAction, toTarget:self.target, withObject: g)
                }
                else if self.tapCount == 2 {
                    NSThread.detachNewThreadSelector(self.doubleAction, toTarget:self.target, withObject: g)
                }
                self.tapCount = 0
            })
        }
    }
    typealias DelayTask = (cancel : Bool) -> ()

    func delayHelper(time:NSTimeInterval, task:()->()) ->  DelayTask? {

        func dispatch_later(block:()->()) {
            dispatch_after(
                dispatch_time(
                    DISPATCH_TIME_NOW,
                    Int64(time * Double(NSEC_PER_SEC))),
                dispatch_get_main_queue(),
                block)
        }

        var closure: dispatch_block_t? = task
        var result: DelayTask?

        let delayedClosure: DelayTask = {
            cancel in
            if let internalClosure = closure {
                if (cancel == false) {
                    dispatch_async(dispatch_get_main_queue(), internalClosure);
                }
            }
            closure = nil
            result = nil
        }

        result = delayedClosure

        dispatch_later {
            if let delayedClosure = result {
                delayedClosure(cancel: false)
            }
        }

        return result;
    }

    func cancel(task:DelayTask?) {
        task?(cancel: true)
    }
}
like image 1
Alexey Yurko Avatar answered Nov 17 '22 18:11

Alexey Yurko