Contents

ezefranca/watchshaker

Native shake detection for Apple Watch, designed for Swift 6 and SwiftUI.

Why WatchShaker

  • Swift 6 strict-concurrency safety with main-actor observable state
  • Native SwiftUI lifecycle through .onWatchShake
  • Independent AsyncStream subscriptions for structured concurrency
  • Peak capture, hysteresis, cooldown, and monotonic sensor timing
  • Device-motion user acceleration with a filtered accelerometer fallback
  • Extended runtime integration without pretending watchOS allows unrestricted background work
  • Codable, Sendable event value types and deterministic Swift Testing coverage

Requirements

  • watchOS 10 or later
  • Swift 6 language mode
  • Xcode 16 or later

The simulator doesn't provide representative motion data. Validate gesture thresholds and background behavior on Apple Watch hardware.

Keep one WatchShaker instance active at a time. The package shares one CMMotionManager, following Core Motion's guidance and avoiding competing sampling rates.

Installation

Add WatchShaker with Xcode's File → Add Package Dependencies, using:

https://github.com/ezefranca/WatchShaker.git

Or add it to a package manifest:

dependencies: [
    .package(
        url: "https://github.com/ezefranca/WatchShaker.git",
        from: "2.0.1"
    )
]

SwiftUI

For most apps, attach the lifecycle-aware modifier to the view that owns the feature:

import SwiftUI
import WatchShaker

struct ContentView: View {
    @State private var shakeCount = 0

    var body: some View {
        Text("Shakes: \(shakeCount)")
            .onWatchShake { shake in
                shakeCount += 1
                print(shake.direction, shake.acceleration)
            }
    }
}

Monitoring starts with the view's task and stops when that task is cancelled.

Observable state and async events

Use an owned @State instance when the UI needs lifecycle or latest-event state. WatchShaker uses Observation, so @StateObject is no longer appropriate.

struct MotionView: View {
    @State private var shaker = WatchShaker(
        configuration: .init(sensitivity: .high)
    )

    var body: some View {
        Text(shaker.latestShake?.direction.rawValue ?? "Ready")
            .task {
                let events = shaker.shakes()

                do {
                    try shaker.start(in: .foreground)
                    for await shake in events {
                        print("Peak: \(shake.acceleration.magnitude) g")
                    }
                } catch {
                    print(error.localizedDescription)
                }

                shaker.stop()
            }
    }
}

isAvailable describes hardware capability. It does not indicate that a shake occurred. Read latestShake, use onShake, or consume shakes() for events.

Configuration

let configuration = WatchShakerConfiguration(
    sensitivity: .normal,
    sampling: .balanced,
    detection: .balanced
)

Higher sensitivity (.high or .veryHigh) recognizes gentler gestures. Sampling offers .lowPower, .balanced, and .responsive; detection offers .responsive, .balanced, and .conservative profiles. Begin with the balanced defaults and tune on real hardware with the movement your app actually teaches.

Raw detector constants are kept out of the normal initializer. Applications with measured, hardware-specific requirements can create validated custom sampling and detection values. See Configuration.

Background execution

Yes—WatchShaker can keep monitoring during an eligible watchOS extended runtime session:

.onWatchShake(mode: .extendedRuntime) { shake in
    handle(shake)
}

Your watch app target must enable Background Modes and select the session type that truthfully matches the feature. watchOS, not the package, grants runtime and enforces time and energy limits. Physical therapy and smart alarm sessions can run in the background; self care and mindfulness sessions remain frontmost. A workout app should own an HKWorkoutSession and use WatchShaker's .foreground mode while that workout grants execution time.

See Background Monitoring before shipping this mode.

Migrating from 1.x

The current API removes ambiguous fake state and adopts modern naming. Deprecated aliases cover many 1.x spellings, but lifecycle and event handling should be migrated deliberately.

See the Migration Guide for a complete mapping.

Documentation

Read the native DocC documentation or browse the versioned copy hosted by the Swift Package Index. The public site also publishes agent-readable documentation and structured package context.

Build the complete DocC site with:

xcodebuild docbuild \
  -scheme WatchShaker \
  -destination 'generic/platform=watchOS Simulator'

Research

WatchShaker was presented at ICECCME 2024. Read the paper on IEEE Xplore or view the preprint.

Contributing and license

Contributions are welcome. Read CONTRIBUTING.md and the security policy before opening a change.

WatchShaker is available under the MIT License. See LICENSE.

Package Metadata

Repository: ezefranca/watchshaker

Default branch: main

README: README.md