CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

90

Quality

90%

Does it follow best practices?

Impact

Average score across 248 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

motion-patterns.mdskills/core-motion/references/

CoreMotion Extended Patterns

Overflow reference for the core-motion skill. Contains advanced patterns that exceed the main skill file's scope.

Contents

  • SwiftUI Integration with @Observable
  • CMBatchedSensorManager (High-Frequency)
  • Headphone Motion
  • Pedometer SwiftUI View
  • Activity-Based Navigation
  • Water Submersion (watchOS)

SwiftUI Integration with @Observable

Motion Manager Service

import CoreMotion
import SwiftUI

@Observable
@MainActor
final class MotionService {
    static let shared = MotionService()

    private let manager = CMMotionManager()

    var pitch: Double = 0
    var roll: Double = 0
    var yaw: Double = 0
    var userAcceleration: CMAcceleration = CMAcceleration()
    var isActive = false

    func startDeviceMotion(interval: TimeInterval = 1.0 / 60.0) {
        guard manager.isDeviceMotionAvailable, !isActive else { return }

        manager.deviceMotionUpdateInterval = interval
        manager.startDeviceMotionUpdates(
            using: .xArbitraryZVertical,
            to: .main
        ) { [weak self] motion, error in
            guard let self, let motion else { return }
            self.pitch = motion.attitude.pitch
            self.roll = motion.attitude.roll
            self.yaw = motion.attitude.yaw
            self.userAcceleration = motion.userAcceleration
        }
        isActive = true
    }

    func stop() {
        manager.stopDeviceMotionUpdates()
        isActive = false
    }
}

SwiftUI View Using Motion

struct TiltView: View {
    @State private var motionService = MotionService.shared

    var body: some View {
        VStack {
            Circle()
                .fill(.blue)
                .frame(width: 60, height: 60)
                .offset(
                    x: motionService.roll * 100,
                    y: motionService.pitch * 100
                )

            Text("Roll: \(motionService.roll, format: .number.precision(.fractionLength(2)))")
            Text("Pitch: \(motionService.pitch, format: .number.precision(.fractionLength(2)))")
        }
        .onAppear { motionService.startDeviceMotion() }
        .onDisappear { motionService.stop() }
    }
}

Level Indicator

struct LevelIndicator: View {
    @State private var motionService = MotionService.shared

    private var isLevel: Bool {
        abs(motionService.pitch) < 0.05 && abs(motionService.roll) < 0.05
    }

    var body: some View {
        ZStack {
            Circle()
                .stroke(isLevel ? .green : .gray, lineWidth: 3)
                .frame(width: 200, height: 200)

            Circle()
                .fill(isLevel ? .green : .red)
                .frame(width: 20, height: 20)
                .offset(
                    x: motionService.roll * 100,
                    y: motionService.pitch * -100
                )
        }
        .onAppear { motionService.startDeviceMotion(interval: 1.0 / 30.0) }
        .onDisappear { motionService.stop() }
    }
}

CMBatchedSensorManager (High-Frequency)

CMBatchedSensorManager delivers batches of high-frequency accelerometer and device-motion data for workout-style motion analysis, such as golf swings or bat swings. The async update sequences are watchOS 10+ APIs; check availability and authorization before starting updates.

AsyncSequence Pattern

import CoreMotion

@Observable
@MainActor
final class BatchedMotionService {
    private let batchedManager = CMBatchedSensorManager()
    private var updateTask: Task<Void, Never>?

    var latestAcceleration: CMAcceleration?

    func startBatchedAccelerometer() {
        let authorization = CMBatchedSensorManager.authorizationStatus
        guard CMBatchedSensorManager.isAccelerometerSupported,
              authorization != .denied,
              authorization != .restricted else { return }

        updateTask = Task {
            for await batch in batchedManager.accelerometerUpdates() {
                guard !Task.isCancelled else { break }
                // Process entire batch
                for sample in batch {
                    // sample.acceleration, sample.timestamp
                }
                // Update UI with most recent
                if let latest = batch.last {
                    latestAcceleration = latest.acceleration
                }
            }
        }
    }

    func stop() {
        updateTask?.cancel()
        updateTask = nil
        batchedManager.stopAccelerometerUpdates()
    }
}

Reading Frequency

let batchedManager = CMBatchedSensorManager()

// Start updates, then read the frequency the device reports.
batchedManager.startAccelerometerUpdates()
let reportedHz = batchedManager.accelerometerDataFrequency

accelerometerDataFrequency and deviceMotionDataFrequency are read-only. Do not assign them; use the reported values to size buffers, throttle UI updates, or downsample processed results.

Headphone Motion

Track head motion using AirPods Pro / AirPods Max via CMHeadphoneMotionManager. On iOS and macOS, include NSMotionUsageDescription. Use connection-status updates when the app needs connect/disconnect events outside an active motion session.

import CoreMotion

@Observable
@MainActor
final class HeadphoneMotionService: NSObject {
    private let headphoneManager = CMHeadphoneMotionManager()

    var isConnected = false
    var headPitch: Double = 0
    var headYaw: Double = 0

    func start() {
        guard headphoneManager.isDeviceMotionAvailable else { return }

        headphoneManager.delegate = self
        headphoneManager.startConnectionStatusUpdates()
        headphoneManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in
            guard let self, let motion else { return }
            self.headPitch = motion.attitude.pitch
            self.headYaw = motion.attitude.yaw
        }
    }

    func stop() {
        headphoneManager.stopDeviceMotionUpdates()
        headphoneManager.stopConnectionStatusUpdates()
    }
}

extension HeadphoneMotionService: CMHeadphoneMotionManagerDelegate {
    nonisolated func headphoneMotionManagerDidConnect(
        _ manager: CMHeadphoneMotionManager
    ) {
        Task { @MainActor in isConnected = true }
    }

    nonisolated func headphoneMotionManagerDidDisconnect(
        _ manager: CMHeadphoneMotionManager
    ) {
        Task { @MainActor in isConnected = false }
    }
}

Pedometer SwiftUI View

Step Counter Dashboard

import CoreMotion
import SwiftUI

@Observable
@MainActor
final class PedometerService {
    private let pedometer = CMPedometer()

    var todaySteps: Int = 0
    var todayDistance: Double = 0
    var floorsAscended: Int = 0

    func fetchToday() {
        guard CMPedometer.isStepCountingAvailable() else { return }

        let startOfDay = Calendar.current.startOfDay(for: Date())

        pedometer.queryPedometerData(from: startOfDay, to: Date()) { [weak self] data, error in
            guard let self, let data else { return }
            Task { @MainActor in
                self.todaySteps = data.numberOfSteps.intValue
                self.todayDistance = data.distance?.doubleValue ?? 0
                self.floorsAscended = data.floorsAscended?.intValue ?? 0
            }
        }
    }

    func startLiveUpdates() {
        guard CMPedometer.isStepCountingAvailable() else { return }

        let startOfDay = Calendar.current.startOfDay(for: Date())

        pedometer.startUpdates(from: startOfDay) { [weak self] data, error in
            guard let self, let data else { return }
            Task { @MainActor in
                self.todaySteps = data.numberOfSteps.intValue
                self.todayDistance = data.distance?.doubleValue ?? 0
                self.floorsAscended = data.floorsAscended?.intValue ?? 0
            }
        }
    }

    func stopLiveUpdates() {
        pedometer.stopUpdates()
    }
}

SwiftUI Dashboard View

struct StepDashboard: View {
    @State private var pedometerService = PedometerService()

    var body: some View {
        List {
            Section("Today") {
                LabeledContent("Steps") {
                    Text("\(pedometerService.todaySteps)")
                }
                LabeledContent("Distance") {
                    Text(
                        Measurement(value: pedometerService.todayDistance, unit: UnitLength.meters),
                        format: .measurement(width: .abbreviated)
                    )
                }
                if CMPedometer.isFloorCountingAvailable() {
                    LabeledContent("Floors Climbed") {
                        Text("\(pedometerService.floorsAscended)")
                    }
                }
            }
        }
        .onAppear { pedometerService.startLiveUpdates() }
        .onDisappear { pedometerService.stopLiveUpdates() }
    }
}

Activity-Based Navigation

Switch between driving and walking modes automatically:

import CoreMotion

@Observable
@MainActor
final class NavigationModeService {
    private let activityManager = CMMotionActivityManager()

    enum Mode: String {
        case walking, driving, cycling, unknown
    }

    var currentMode: Mode = .unknown

    func startMonitoring() {
        guard CMMotionActivityManager.isActivityAvailable() else { return }

        activityManager.startActivityUpdates(to: .main) { [weak self] activity in
            guard let self, let activity,
                  activity.confidence != .low else { return }

            Task { @MainActor in
                if activity.automotive {
                    self.currentMode = .driving
                } else if activity.cycling {
                    self.currentMode = .cycling
                } else if activity.walking || activity.running {
                    self.currentMode = .walking
                } else if activity.stationary {
                    // Keep previous mode when stationary (e.g., at a stoplight)
                }
            }
        }
    }

    func stopMonitoring() {
        activityManager.stopActivityUpdates()
    }
}

Water Submersion (watchOS)

Track water depth and temperature for dive apps on supported Apple Watch hardware. Use waterSubmersionAvailable rather than hard-coding model checks: Apple Watch Ultra supports submersion data, and Apple Watch Series 10 supports the Shallow Depth and Pressure capability.

Setup checklist:

  • Add NSMotionUsageDescription.
  • Add the Shallow Depth and Pressure capability for dives up to 6 meters, or apply for the full Submerged Depth and Pressure entitlement for dives up to 40 meters.
  • Add WKBackgroundModes with underwater-depth so the app can remain frontmost and eligible for dive autolaunch.
  • Check availability before instantiating CMWaterSubmersionManager.
import CoreMotion

@Observable
@MainActor
final class DiveService: NSObject {
    private var submersionManager: CMWaterSubmersionManager?

    var isSubmerged = false
    var currentDepth: Double?
    var waterTemperature: Double?

    func start() {
        guard CMWaterSubmersionManager.waterSubmersionAvailable else { return }
        let manager = CMWaterSubmersionManager()
        manager.delegate = self
        submersionManager = manager
    }
}

extension DiveService: CMWaterSubmersionManagerDelegate {
    nonisolated func manager(
        _ manager: CMWaterSubmersionManager,
        didUpdate event: CMWaterSubmersionEvent
    ) {
        Task { @MainActor in
            isSubmerged = event.state == .submerged
        }
    }

    nonisolated func manager(
        _ manager: CMWaterSubmersionManager,
        didUpdate measurement: CMWaterSubmersionMeasurement
    ) {
        Task { @MainActor in
            currentDepth = measurement.depth?.value
        }
    }

    nonisolated func manager(
        _ manager: CMWaterSubmersionManager,
        didUpdate temperature: CMWaterTemperature
    ) {
        Task { @MainActor in
            waterTemperature = temperature.temperature.value
        }
    }

    nonisolated func manager(
        _ manager: CMWaterSubmersionManager,
        errorOccurred error: any Error
    ) {
        print("Submersion error: \(error)")
    }
}

Important: CMWaterSubmersionManager requires the Shallow Depth and Pressure capability or the full Submerged Depth and Pressure entitlement. If the app lacks the entitlement, the delegate receives CMError.notEntitled and no submersion data.

skills

core-motion

README.md

tile.json