TAP

iOS SDK

Native iOS input via TAPKit. Pair in Settings first — TAPKit does not scan.

  1. Package
  2. Install
  3. Hello Tap
  4. TAPKitDelegate
  5. TAPInputMode
  6. Combinations
  7. Haptics
  8. Raw sensors
  9. TapXR Spatial Control
  10. Protocol versions
  11. V2 configuration
  12. Logging

Package

SourceTapWithUs/tap-ios-sdk
LicenseApache 2.0
StatusOfficial
LanguageSwift (Obj-C supported)
RequiresiOS 11.2+ (15.6+ recommended for the example app)

Install

Clone the repo, open TAPKit.xcodeproj, build the TAPKit scheme, then embed TAPKit.framework (Embed & Sign). The Xcode project also ships TAPKit-Example and TAPKitUnityBridge.

Add to your app’s Info.plist:

Info.plist
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth is required to connect to TAP devices.</string>
Import
import TAPKit

Objective-C: #import <TAPKit/TAPKit-Swift.h>.

Hello Tap

Call start() once (typically when your main screen appears). Add one or more TAPKitDelegate implementations before or after start().

HelloTapViewController.swift
import UIKit
import TAPKit

class HelloTapViewController: UIViewController, TAPKitDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Device must already be paired in iOS Settings → Bluetooth.
        TAPKit.log.disableAllEvents()          // optional
        TAPKit.sharedKit.addDelegate(self)
        TAPKit.sharedKit.setDefaultTAPInputMode(.controller(), immediate: true)
        TAPKit.sharedKit.setDefaultTAPXRState(TAPXRState.airMouse(), applyImmediate: true)  // TapXR only
        TAPKit.sharedKit.start()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        TAPKit.sharedKit.removeDelegate(self)
    }

    func tapConnected(withIdentifier identifier: String, name: String) {
        print("Connected: \(name) (\(identifier))")
    }

    func tapped(identifier: String, combination: UInt8, multitap: UInt8) {
        let fingers = TAPCombination.toFingers(combination)
        print("Tap from \(identifier): \(fingers), multitap: \(multitap)")
    }

    func moused(identifier: String, velocityX: Int16, velocityY: Int16, isMouse: Bool) {
        guard isMouse else { return }
        print("mouse", velocityX, velocityY)
    }
}

TAPKitDelegate

All methods are optional. Multiple delegates are supported. Register with addDelegate. tapped now includes multitap (1–3 when applicable). Hold events fire only in Tap Hold mode. Serial number and standby are V2 devices.

TAPKitDelegate
@objc public protocol TAPKitDelegate : class {
    @objc optional func tapConnected(withIdentifier identifier: String, name: String)
    @objc optional func tapDisconnected(withIdentifier identifier: String)
    @objc optional func tapFailedToConnect(withIdentifier identifier: String, name: String)

    @objc optional func tapped(identifier: String, combination: UInt8, multitap: UInt8)
    @objc optional func moused(identifier: String, velocityX: Int16, velocityY: Int16, isMouse: Bool)
    @objc optional func rawSensorDataReceived(identifier: String, data: RawSensorData)

    @objc optional func tapChangedAirGesturesState(identifier: String, isInAirGesturesState: Bool)
    @objc optional func tapAirGestured(identifier: String, gesture: TAPAirGesture)
    @objc optional func tapXRAirGestured(identifier: String, gesture: TAPXRAirGesture)

    @objc optional func tapDidReadHardwareVersion(identifier: String, hw: Int)
    @objc optional func tapDidReadFirmwareVersion(identifier: String, fw: Int)
    @objc optional func tapDidReadBatteryLevel(identifier: String, batteryLevel: Int)

    @objc optional func tapHoldStarted(identifier: String, combination: UInt8)
    @objc optional func tapHoldEnded(identifier: String, combination: UInt8)

    @objc optional func tapDidChangeOrientation(roll: Int, pitch: Int, yaw: Int)

    @objc optional func tapChangedStandbyState(identifier: String, isInStandby: Bool)
    @objc optional func tapDidReadSerialNumber(identifier: String, serialNumber: String)
}
swift
TAPKit.sharedKit.addDelegate(self)
TAPKit.sharedKit.start()
// …
TAPKit.sharedKit.removeDelegate(self)

Connected taps: TAPKit.sharedKit.getConnectedTaps() → dictionary of identifier → display name. Version and battery reads run on connect; you can also call readHardwareVersion(), readFirmwareVersion(), and readBatteryLevel(). HW/FW integers are MMmmbb (example: 30200 → 3.2.0).

TAPInputMode

  • Controller (default) — tapped / moused
  • Text — HID keyboard; SDK tap callbacks are not fired
  • Controller with Mouse HID — controller + system mouse (AssistiveTouch cursor on iOS 13+)
  • Controller with Full HID — full HID controller mode
  • Tap Hold — short tap vs long press via tapHoldStarted / tapHoldEnded
  • Raw Sensor — gyro / accelerometer stream
  • V2 debug — enables all V2 features at once (raw IMU + model detection + IMU motion + air-gesture vision stream)
swift
TAPKit.sharedKit.setTAPInputMode(TAPInputMode.text(), forIdentifiers: nil)
TAPKit.sharedKit.setTAPInputMode(TAPInputMode.controller(), forIdentifiers: nil)
TAPKit.sharedKit.setTAPInputMode(TAPInputMode.tapHold(), forIdentifiers: nil)
TAPKit.sharedKit.setDefaultTAPInputMode(TAPInputMode.controller(), immediate: true)
let mode = TAPKit.sharedKit.getTAPInputMode(identifier: uuid)

Pass nil for forIdentifiers to apply to all connected devices. Newly connected devices receive the default set via setDefaultTAPInputMode(_:immediate:).

Combinations

combination is UInt8 in 1…31. Bit0 = thumb … bit4 = pinky. Example: 5 → thumb + middle.

swift
let fingers = TAPCombination.toFingers(combination)
let combo = TAPCombination.fromFingers(thumb, index, middle, ring, pinky)
let names = TAPCombination.combinationSpeakableString(for: combination)

Haptics

Up to 18 durations (ms) as haptic, pause, haptic, pause… Null/missing identifiers = all connected taps.

swift
TAPKit.sharedKit.vibrate(durations: [500, 100, 500], forIdentifiers: nil)

Raw sensors

Five 3-axis finger accelerometers + thumb IMU (Tap Strap 2). Stream ~200 messages/minute. Sensitivities: deviceAccelerometer 1–4, imuGyro 1–4, imuAccelerometer 1–5. On V2 devices you can also change IMU sensitivity on the fly with setIMUSensitivity without re-entering the mode.

swift
let sensitivity = TAPRawSensorSensitivity(
    deviceAccelerometer: 2, imuGyro: 2, imuAccelerometer: 3)
TAPKit.sharedKit.setTAPInputMode(
    TAPInputMode.rawSensor(sensitivity: sensitivity), forIdentifiers: nil)

func rawSensorDataReceived(identifier: String, data: RawSensorData) {
    switch data.type {
    case .Device:
        if let thumb = data.getPoint(for: RawSensorData.iDEV_THUMB) {
            print("Thumb accel:", thumb.x, thumb.y, thumb.z)
        }
    case .IMU:
        if let gyro = data.getPoint(for: RawSensorData.iIMU_GYRO) {
            print("Gyro:", gyro.x, gyro.y, gyro.z)
        }
    default:
        break
    }
}

TapXR Spatial Control

TAPXRState is separate from input mode: userControl() (user switches air-mouse / tapping), airMouse(), tapping(), or dontSend(). applyImmediate: true applies the default to already-connected devices.

swift
TAPKit.sharedKit.setDefaultTAPXRState(TAPXRState.userControl(), applyImmediate: true)
TAPKit.sharedKit.setTAPXRState(TAPXRState.airMouse(), forIdentifiers: ["device-uuid"])

Spatial gestures arrive on tapXRAirGestured as TAPXRAirGesture: click / drag / potential-drag per finger, drop, fist begin/end, and swipe left/right/up/down. Prefer this over the XR cases on the legacy TAPAirGesture enum. Spatial control may require authorization on some firmware — request access.

Protocol versions

TAP firmware speaks one of two BLE protocols. TAPKit detects which one per device on connect — no configuration needed. The same delegate callbacks and TAPInputMode / TAPXRState calls apply; TAPKit translates under the hood.

Legacy (v1)Classic characteristic-per-event. Hardware: Tap Strap, Tap Strap 2, TapXR.
V2 (framed)One framed read/write pair. Hardware: TapXR with newer firmware. Adds device features, vision-sensor control, standby, serial number, keepalive.

V2-only APIs (below) are ignored on legacy devices (a warning is logged). This split matches tap-python-sdk v1 (TapSDK) vs v2 (TapSDK2).

V2 configuration

Direct V2 primitives. Input modes also write these features under the hood — use the direct APIs for fine-grained control or read-back. Getters take an optional timeout: (default 2s) and run completions on the main queue.

  • .rawIMUData — raw IMU packets via rawSensorDataReceived
  • .modelDetection — on-device tap / air-gesture models
  • .imuMotionData — IMU motion (moused, tapDidChangeOrientation)
  • .standbyGestureDetection — wake-gesture while in standby
V2
TAPKit.sharedKit.setFeature(.modelDetection, enabled: true, forIdentifiers: [uuid])
TAPKit.sharedKit.getFeature(.modelDetection, forIdentifier: uuid) { enabled in
    print("modelDetection:", String(describing: enabled))  // nil on timeout
}

TAPKit.sharedKit.setVisionSensorOpMode(.stream, forIdentifiers: [uuid])   // .trigger / .streamOnTrigger / .stream
TAPKit.sharedKit.setVisionSensorModel(.airGesture, forIdentifiers: [uuid]) // .tapping / .airGesture

TAPKit.sharedKit.setIMUSensitivity(gyro: 2, accelerometer: 1, forIdentifiers: [uuid])  // gyro 0–5, accel 0–4
TAPKit.sharedKit.setStandbyState(true, forIdentifiers: [uuid])
TAPKit.sharedKit.readSerialNumber(forIdentifiers: nil)

Background: sendModeInBackground defaults to false (SDK switches to text mode + user-control XR on background). V2 keepalive is sent automatically while the app is active. set* commands and the V2 enums are Obj-C visible; get* read-backs are Swift-only.

Logging

Levels: .warning, .error, .info, .fatal.

swift
TAPKit.log.enableAllEvents()
TAPKit.log.enable(event: .error)
TAPKit.log.disableAllEvents()