Single and double taps on UITableViewCell in Swift 3
Details
- Xcode 10.2.1 (10E1001), Swift 5
Solution
protocol MultiTappableDelegate: class {
func singleTapDetected(in view: MultiTappable)
func doubleTapDetected(in view: MultiTappable)
}
class ThreadSafeValue<T> {
private var _value: T
private lazy var semaphore = DispatchSemaphore(value: 1)
init(value: T) { _value = value }
var value: T {
get {
semaphore.signal(); defer { semaphore.wait() }
return _value
}
set(value) {
semaphore.signal(); defer { semaphore.wait() }
_value = value
}
}
}
protocol MultiTappable: UIView {
var multiTapDelegate: MultiTappableDelegate? { get set }
var tapCounter: ThreadSafeValue<Int> { get set }
}
extension MultiTappable {
func initMultiTap() {
if let delegate = self as? MultiTappableDelegate { multiTapDelegate = delegate }
let tap = UITapGestureRecognizer(target: self, action: #selector(UIView.multitapActionHandler))
addGestureRecognizer(tap)
}
func multitapAction() {
if tapCounter.value == 0 {
DispatchQueue.global(qos: .utility).async {
usleep(250_000)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.tapCounter.value > 1 {
self.multiTapDelegate?.doubleTapDetected(in: self)
} else {
self.multiTapDelegate?.singleTapDetected(in: self)
}
self.tapCounter.value = 0
}
}
}
tapCounter.value += 1
}
}
private extension UIView {
@objc func multitapActionHandler() {
if let tappable = self as? MultiTappable { tappable.multitapAction() }
}
}
Usage
class MyView: UIView, MultiTappable {
weak var multiTapDelegate: MultiTappableDelegate?
lazy var tapCounter = ThreadSafeValue(value: 0)
override func awakeFromNib() {
super.awakeFromNib()
initMultiTap()
}
}
Full sample
ViewController.swift
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.tableFooterView = UIView()
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int { return 1 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 10 }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell") as! TableViewCell
cell.label.text = "\(indexPath)"
cell.delegate = self
return cell
}
}
extension ViewController: TableViewCellDelegate {
func singleTapDetected(in cell: TableViewCell) {
if let indexPath = tableView.indexPath(for: cell) { print("singleTap \(indexPath) ") }
}
func doubleTapDetected(in cell: TableViewCell) {
if let indexPath = tableView.indexPath(for: cell) { print("doubleTap \(indexPath) ") }
}
}
TableViewCell.swift
import UIKit
protocol TableViewCellDelegate: class {
func singleTapDetected(in cell: TableViewCell)
func doubleTapDetected(in cell: TableViewCell)
}
class TableViewCell: UITableViewCell, MultiTappable {
weak var multiTapDelegate: MultiTappableDelegate?
lazy var tapCounter = ThreadSafeValue(value: 0)
@IBOutlet weak var label: UILabel!
weak var delegate: TableViewCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
initMultiTap()
}
}
extension TableViewCell: MultiTappableDelegate {
func singleTapDetected(in view: MultiTappable) { self.delegate?.singleTapDetected(in: self) }
func doubleTapDetected(in view: MultiTappable) { self.delegate?.doubleTapDetected(in: self) }
}
Main.storyboard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12118" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="g2V-T0-sqD">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12086"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="stackoverflow_43153530" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="fQm-mQ-a9u">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="TableViewCell" id="nsF-ue-0bK" customClass="TableViewCell" customModule="stackoverflow_43153530" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="nsF-ue-0bK" id="pT6-2N-oTC">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fEK-J3-oqH">
<rect key="frame" x="8" y="8" width="42" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="fEK-J3-oqH" firstAttribute="leading" secondItem="pT6-2N-oTC" secondAttribute="leadingMargin" id="Vfg-Ij-f6c"/>
<constraint firstItem="fEK-J3-oqH" firstAttribute="top" secondItem="pT6-2N-oTC" secondAttribute="topMargin" id="tc0-qJ-N1n"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="label" destination="fEK-J3-oqH" id="YBJ-tG-J5T"/>
</connections>
</tableViewCell>
</prototypes>
</tableView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="fQm-mQ-a9u" firstAttribute="bottom" secondItem="wfy-db-euE" secondAttribute="top" id="8Vy-l8-jpB"/>
<constraint firstItem="fQm-mQ-a9u" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" id="Wwr-ox-Qbd"/>
<constraint firstItem="fQm-mQ-a9u" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="-64" id="xJR-Uk-rbj"/>
<constraint firstAttribute="trailing" secondItem="fQm-mQ-a9u" secondAttribute="trailing" id="zxs-ED-Whb"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="pLJ-Bz-NIm"/>
<connections>
<outlet property="tableView" destination="fQm-mQ-a9u" id="DhZ-jj-zmB"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1079.2" y="137.18140929535232"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="w7e-Wj-oUR">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="g2V-T0-sqD" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="7qG-8v-S0O">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="yqZ-pK-Yf3"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="tnz-x0-vDN" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="140" y="137.18140929535232"/>
</scene>
</scenes>
</document>
Result
ONLY double tap on UITableView Swift 3.0
To achieve your goal:
- Add tap gesture recognizer on your table view, do not forget to set numberOfTapsRequired = 2
- Do not implement didSelectRowAtIndexPath method
- To prevent table view cells from changing their background color after single tap, set in interface builder, Attributes Inspector tab, table view "selection" attribute to "No selection" or table view cell "selection" attribute to "None".
If you want to get indexpath of cell being doubletapped, in your gesture recognizer handler method get tap location in tap.view and use indexPathForRowAtPoint method of tableView:
let tapLocationPoint = tap.location(in: tap.view)
let tappedCellIndexPath = tableView.indexPathForRow(at: tapLocationPoint)
How can I detect a double tap on a certain cell in UITableView?
If you do not want to create a subclass of UITableView
, use a timer with the table view's didSelectRowAtIndex:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
//checking for double taps here
if(tapCount == 1 && tapTimer != nil && tappedRow == indexPath.row){
//double tap - Put your double tap code here
[tapTimer invalidate];
[self setTapTimer:nil];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Double Tap" message:@"You double-tapped the row" delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
[alert show];
[alert release];
}
else if(tapCount == 0){
//This is the first tap. If there is no tap till tapTimer is fired, it is a single tap
tapCount = tapCount + 1;
tappedRow = indexPath.row;
[self setTapTimer:[NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(tapTimerFired:) userInfo:nil repeats:NO]];
}
else if(tappedRow != indexPath.row){
//tap on new row
tapCount = 0;
if(tapTimer != nil){
[tapTimer invalidate];
[self setTapTimer:nil];
}
}
}
- (void)tapTimerFired:(NSTimer *)aTimer{
//timer fired, there was a single tap on indexPath.row = tappedRow
if(tapTimer != nil){
tapCount = 0;
tappedRow = -1;
}
}
HTH
Double tap on cell in UITableview to perform segue
Turns out, the selection style of my cell is set to none.
cell.selectionStyle = UITableViewCellSelectionStyle.none
I don't want a selection style though, so the way around this is to make an asynchronous call
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ShowEventDetailsSegue", sender: nil)
}
}
This now fixes the issue. For more info, check out this post.
Related Topics
Creating a Future Date in Swift with Nsdate()
How to Test That Statictexts Contains a String Using Xctest
Switch That Checks Nsindexpath's Row and Section
Uiimage Created from Mtkview Results in Color/Opacity Differences
Swiftui: How to Iterate Over an Array of Bindable Objects
Xcode: Could Not Load Modelio.Framework, Scenekit.Framework, etc
How to Get Current Location with Swiftui
What Is Import Func, Struct, Class, and @_Exported in Swift
Swift 3 and Firebase: Retrieve Auto Id Value from Snapshot
Swift Diffabledatasource Make Insert&Delete Instead of Reload
Convert or Cast Object to String
Sprite-Kit: Moving an Element in Circular Path
Swift, Avaudiorecorder: Error 317: Ca_Debug_String: Inpropertydata == Null
Using Uiactivityindicatorview with Uiwebview in Swift
Swift Await/Async - How to Wait Synchronously for an Async Task to Complete