TechIDaily Journal
The 90-Second Reset: Inside StretchGoGo's Posture Engine
StretchGoGo detects your posture through CoreMotion and delivers 30-second stretches on your iPhone and Apple Watch. This engineering deep-dive unpacks the SwiftUI + CoreMotion + HealthKit + WatchOS architecture behind a privacy-first, on-device stretch coach — including the seven-state posture classifier, the hand-up gesture pipeline, and the 90-day numbers we learned to trust.
The 90-Second Reset: How StretchGoGo Detects Your Posture and Delivers Personalized Stretches On-Device
*A deep-dive into the SwiftUI + CoreMotion + HealthKit + WatchOS architecture that makes a 30-second daily reset feel native to every iPhone and Apple Watch.*
If you have ever finished a Pomodoro, glanced at the clock, and realized that your shoulders have quietly climbed toward your ears for the last fifty minutes, this post is for you. StretchGoGo is the app we built to interrupt that pattern. It reads your actual posture from the motion sensors in your pocket or on your wrist, picks one of seven body states, and delivers a 30-second stretch on the device that is closest to you. Nothing is uploaded. Nothing leaves the phone. In this post we will walk through the entire engineering pipeline that makes that possible, share the real numbers after ninety days in production, and be honest about the five things we got wrong along the way.
1. Why On-Device Privacy Matters
Most fitness apps follow the same playbook. They stream accelerometer and gyroscope data to a cloud service, run a model there, and push a notification back to your phone. That is fine for steps and heart rate, but it is the wrong shape for posture. Posture is a continuous, low-signal, high-context stream. Sending every sample off-device burns battery, leaks a behavioral fingerprint, and adds a network round trip that pushes latency past the threshold where a stretch reminder feels natural.
StretchGoGo takes the opposite bet: every inference runs locally. CoreMotion samples are read by a CoreML model on the iPhone SoC, and a paired WatchOS extension runs the same model on the watch neural engine. HealthKit is the only system service that touches the data, and even then the writes stay inside the user's Health database. There is no backend posture service. There is no telemetry endpoint. The privacy story is not a marketing claim, it is a structural property of the design, and that property shapes every architectural decision in this post.
2. Architecture: iPhone + Apple Watch, Two Sides of One Brain
The app is split into three Swift packages that share a single source of truth for the posture classifier.
StretchGoGoApp (iOS, SwiftUI)
├── MotionService (CoreMotion reader, 50 Hz)
├── PostureClassifier (CoreML, 7 states, on-device)
├── RoutineEngine (JSON routine selection, 3 difficulties)
└── HealthBridge (HKWorkoutSession, HKLiveWorkoutBuilder)
StretchGoGoWatch (watchOS, SwiftUI)
├── WCSessionBridge (mirrors classifier state)
├── HandUpGesture (Crown + raise-to-talk)
└── TapticCoach (WKHapticPatternEngine)
SharedCore (Swift Package)
├── Models (PostureState, StretchRoutine, StretchStep)
├── Filters (Butterworth 5 Hz, gravity subtract)
└── Utilities (HapticPattern, RoutineDifficulty)The iPhone side owns the heavy lifting because it has the battery budget. The watch side owns the immediate response because it is on your wrist. WCSession is the bus between them, but we deliberately use it as a state mirror rather than a request-response channel. The watch predicts locally, the iPhone predicts locally, and a lightweight reconciliation step runs when both sides are awake. That decoupling means a watch with no paired phone still works in airplane mode.
3. CoreMotion: Reading the Body
CoreMotion gives you three primitives: accelerometer (high-frequency linear acceleration), gyroscope (angular velocity), and device motion (a fused attitude estimate). For posture you need all three, and you need them at a sample rate that is high enough to catch a sit-to-stand transition without burning the battery.
Our final pipeline samples at 50 Hz, applies a 5 Hz low-pass Butterworth filter to remove fan and motor noise, subtracts gravity using the fused attitude vector, and feeds the resulting linear acceleration and rotation rate into a 64-sample sliding window. That window is then handed to the classifier.
import CoreMotion
final class MotionService {
private let manager = CMMotionManager()
private let queue = OperationQueue()
private let bufferSize = 64
private var samples: [MotionSample] = []
func start() {
manager.deviceMotionUpdateInterval = 1.0 / 50.0
manager.startDeviceMotionUpdates(to: queue) { [weak self] motion, _ in
guard let self, let motion else { return }
let sample = MotionSample(
timestamp: motion.timestamp,
userAccel: motion.userAcceleration,
rotation: motion.rotationRate
)
self.samples.append(sample)
if self.samples.count >= self.bufferSize {
let window = Array(self.samples.suffix(self.bufferSize))
self.classifier?.ingest(window)
}
}
}
func stop() { manager.stopDeviceMotionUpdates() }
}The trick that took us the longest to find was the queue. CoreMotion callbacks come on whatever queue you pass in, and if that queue is also doing SwiftUI work, you will drop frames. A dedicated serial OperationQueue with QoS .userInitiated is enough to keep the buffer stable even on an iPhone 12 mini under thermal pressure.
4. The Seven-State Posture Classifier
The model is intentionally small. We classify posture into seven states: sitting, standing, walking, running, lying, driving, and exercising. We tried CreateML first because it is the easy on-ramp. It trained a 92 percent accurate model in an afternoon, but it could not tell sitting in a car from sitting in a chair, because the accelerometer signature is identical. We ended up rolling our own Bayesian classifier that fuses CoreMotion with HealthKit workout state and the iOS Focus mode.
The confusion matrix after three months of in-the-wild collection is honest. Sitting versus standing is at 96 percent. Sitting versus driving is at 89 percent. Lying versus sitting is the worst pair at 84 percent, and that is mostly because people fall asleep on the couch at 11pm and the classifier gets confused. We added a time-of-day prior and that pulled lying up to 91 percent without touching the rest of the matrix.
5. HealthKit: Talking to Apple's Body Data
HealthKit is the only data store StretchGoGo writes to. We open a workout session when the classifier detects exercising or a user-initiated routine, and we let HKLiveWorkoutBuilder stream heart rate, heart rate variability, and active energy into the system. We never read health data outside of an active session, which keeps the privacy nutrition label clean and avoids the awkward "we read your health data for advertising" review that gets apps rejected under Guideline 5.1.1.
The permission flow is the other piece worth sharing. Asking for all HealthKit types up front is the surest way to get a 50 percent denial rate. We ask for read access only when a feature needs it, and we ask for write access only when the user actually starts a workout. The denial rate drops to under 12 percent with that sequencing, and the App Store review team has never questioned the flow.
6. WatchOS: A Coach On Your Wrist
The watch app is not a mirror of the phone app. It runs the same classifier on the watch neural engine, listens for the hand-up gesture through the crown sensor, and fires a haptic pattern within 80 milliseconds of a posture transition. That latency matters. Anything north of 200 milliseconds feels like a notification. Anything south of 100 milliseconds feels like your own body reminding you.
Digital Crown control is the single best input we added. Users can scroll through a stretch preview without taking their other hand off the keyboard, and a hard press starts the routine. We measured a 38 percent completion rate for stretches started from the watch versus 24 percent for stretches started from the phone, and the gap is almost entirely because the watch removes the friction of unlocking and tapping.
7. The 30-Second Routine Engine
Every routine is a JSON document that lists a sequence of StretchStep objects. Each step has a duration, a target body region, an animation key, and a difficulty. The RoutineEngine picks the next routine based on the current posture state, the time of day, and a rolling seven-day fatigue score derived from completed stretches.
struct StretchStep: Codable, Identifiable {
let id: UUID
let region: BodyRegion // neck, shoulders, lowerBack, hips, wrists, ankles
let durationSeconds: Int // 10 to 25
let difficulty: StretchDifficulty // beginner, intermediate, advanced
let animationKey: String // matches a Lottie animation in the asset bundle
}
struct StretchRoutine: Codable, Identifiable {
let id: UUID
let name: String
let trigger: PostureState // which posture state triggers this routine
let steps: [StretchStep]
let estimatedDurationSeconds: Int
}Three difficulty bands ship by default. Beginners get longer holds with breathing cues, intermediates get standard holds with form tips, and advanced users get a short fast-paced flow. The dynamic difficulty switch is keyed on the seven-day completion count: under fifteen completions per week keeps you in beginner, fifteen to thirty moves you up, and over thirty unlocks advanced.
8. Real-World Numbers: A 90-Day Postmortem
After ninety days we have anonymized data from about fourteen thousand active installs across the United States, Canada, the United Kingdom, Germany, and Australia. The numbers that surprised us:
- Day-30 retention is 41 percent. Industry average for fitness apps is 25 percent.
- Average session length is 94 seconds, including the 30-second stretch plus the 60-second onboarding of the next posture state.
- 62 percent of stretches are started from the watch, not the phone.
- Premium conversion from free trial is 7.4 percent, which is lower than we hoped but higher than our pre-launch model predicted.
- The single most popular routine is the "desk reset" at 3pm, which confirms the WFH hunch.
The numbers we do not share are the ones that would make the post look better. We have a 3.1 percent refund rate on the annual subscription, mostly from users who picked the wrong difficulty and never opened the app to switch it. That is a real problem and it is on our roadmap.
9. Five Things We Got Wrong
We shipped a few things that did not work and we want to be specific about them.
WatchOS background termination. Our first build assumed the watch extension could keep sampling in the background indefinitely. It cannot. Apple gives you a budget of about four seconds of CPU per clock tick, and posture sampling burned through that in two. We added a heartbeat pattern that only samples when the wrist is raised and that bought us most of the day.
HealthKit permission denial. Asking for everything up front gave us a 50 percent denial rate. The sequenced ask described in section 5 brought that to 12 percent. Lesson: every permission is a yes-or-no vote, and the order matters.
CoreMotion at 100 Hz. We started at 100 Hz "just to be safe." Battery drain was unacceptable. The 50 Hz rate gives us the same classification accuracy because posture changes are slow compared to step counting. Lesson: the fastest sample rate is rarely the right one.
Stretch misclassification at 12 percent. Our first classifier confused "reaching for the coffee cup" with "standing desk stretch" about 12 percent of the time. We added a debounce window of four seconds before a stretch fires, which felt annoying at first but reduced the false positive rate to under 3 percent. Lesson: a missed stretch is fine, a wrong stretch is annoying.
IAP refund peak. Two weeks after every release we see a small refund spike, mostly from users who upgraded to a difficulty they did not enjoy. We added a guided difficulty picker in onboarding and the refund rate dropped by 38 percent. Lesson: friction at the moment of purchase is cheaper than friction at the moment of refund.
10. What's Next
Three things are in active development. First, a visionOS build that anchors a stretch coach to your physical desk space using the shared world anchor. Second, an iPad layout that turns the stretch into a side-by-side routine for two people at the same desk. Third, a family sharing tier that lets one subscription cover up to five family members, which is the most-requested feature in our support inbox.
If any of this resonates with the way you work, StretchGoGo has a seven-day free trial with no commitment. The trial is the only honest way to know whether the 30-second reset fits into your day, and we would rather you find that out for yourself than read another marketing paragraph.
*StretchGoGo is designed for general ergonomics and daily movement, not medical treatment. If you have an injury, a chronic condition, or are recovering from surgery, please consult a qualified clinician before using any fitness or stretching app.*