Double tap for UITableRows

I build a lot of UIs in Scriptable using UITables, Recently I was wondering if it would be possible to implement double tap detection (or triple, quadruple, etc), which would allow for much more intricate user interaction.

It turned out to be easier than I expected. Here’s the code that is called whenever a row is tapped/selected:

/** Allowed time between clicks */
const CLICK_INTERVAL = 200;

/** @typedef {1 | 2} ValidTapCount */
/** @typedef {{ [key in ValidTapCount]?: () => any }} ClickMap */

const executeTapListener = (() => {
  let tapCount = 0;
  const clickTimer = new Timer();
  clickTimer.timeInterval = CLICK_INTERVAL;

  /** @param {ClickMap} clickMap */
  return clickMap => {
    const maxClicks = Math.max(
      ...Object.keys(clickMap).map(numStr => parseInt(numStr, 10))
    );
    // Every time a tap comes in, restart the timer & increment the counter
    tapCount++;
    clickTimer.invalidate();
    const executeFn = () => {
      clickMap[String(tapCount)]!();
      tapCount = 0;
    };
    // The timer callback will only ever fire if a click timer reaches its full
    // duration
    maxClicks <= tapCount ? executeFn() : clickTimer.schedule(executeFn);
  };
})();

In short, every time a row is tapped, the tapCount counter goes up to indicate what tap number last occurred. If it is possible that the user will click again, it starts a timer to wait for another tap. If no further taps occur, or if the last tap was the max number of taps being listened to (as defined by the clickMap parameter), the function associated with the current tap number is called.

Here’s an example of what is passed in as clickMap:

const clickMap = {
  1: () => console.log('Single tap detected'),
  2: () => console.log('Double tap detected')
}

So if you passed in the above object, single tap would be delayed by 200ms (the interval that works for me) to make sure it’s not actually a double tap. However passing in the following would result in instant single taps:

const clickMap = {
  1: () => console.log('Single tap detected')
}

I recorded an example, but it is really hard to demo since you can’t see screen taps in the recording: Double tap in Scriptable - Album on Imgur. The text shows the result of the last screen interaction – so after a single tap it waits 200ms and then displays “single tap”, etc.

I haven’t tested, but you should be able to extend this to listen for as many clicks as you like by passing in more attributes in the clickMap:

const clickMap = {
  1: () => console.log('Single tap detected'),
  2: () => console.log('Double tap detected'),
  3: () => console.log('Triple tap detected')
}

It just gets unusable at some point.

One use case I’ve found for this so far is to use single tap for bulk selection in a list of items (e.g. email threads), and double tap for opening them.