Conversation
The balloon icon in the conversations toolbar now responds to device tilt via CoreMotion. When the user tilts the phone left/right, the balloon smoothly sways following gravity (max ±30°). On simulator, a subtle idle sway keeps the balloon animating. Uses exponential smoothing for a floaty, natural feel with ~280ms lag to 63% of target angle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Claude finished @onefovrth's task —— View job Code Review Complete ✓Todo List
Review Feedback✅ StrengthsArchitecture & Design:
Code Quality:
🔍 Issues & Suggestions1. Thread Safety Issue in The device motion updates deliver to // Current (line 48-55):
motionManager.startDeviceMotionUpdates(using: .xArbitraryZVertical, to: .main) { [weak self] motion, _ in
Task { @MainActor [weak self] in
guard let self, let gravity = motion?.gravity else { return }
// ...
}
}
// Suggested:
motionManager.startDeviceMotionUpdates(using: .xArbitraryZVertical, to: .main) { [weak self] motion, _ in
guard let self, let gravity = motion?.gravity else { return }
// Already on main queue, no Task wrapper needed
let rawAngle: Double = asin(max(-1, min(1, gravity.x))) * (180.0 / .pi)
let clamped: Double = max(-Constant.maxAngle, min(Constant.maxAngle, rawAngle))
self.rollAngle = self.rollAngle * (1 - Constant.smoothingFactor) + clamped * Constant.smoothingFactor
}Why: The 2. Same Issue in Timer Callback ( // Current (line 60-67):
idleSwayTimer = Timer.scheduledTimer(withTimeInterval: Constant.updateInterval, repeats: true) { [weak self] _ in
Task { @MainActor [weak self] in
guard let self else { return }
// ...
}
}
// Suggested:
idleSwayTimer = Timer.scheduledTimer(withTimeInterval: Constant.updateInterval, repeats: true) { [weak self] _ in
guard let self else { return }
self.idleSwayPhase += Constant.idleSwaySpeed
self.rollAngle = sin(self.idleSwayPhase) * Constant.idleSwayPrimaryAmplitude
+ sin(self.idleSwayPhase * 0.7) * Constant.idleSwaySecondaryAmplitude
}Why: 3. Reference Count Can Go Negative ( func stopUpdates() {
activeListenerCount -= 1
guard activeListenerCount <= 0 else { return }
activeListenerCount = 0 // Reset to prevent negative valuesIssue: If Suggested: func stopUpdates() {
guard activeListenerCount > 0 else { return }
activeListenerCount -= 1
guard activeListenerCount == 0 else { return }
motionManager.stopDeviceMotionUpdates()
startIdleSway()
}4. Idle Sway Doesn't Stop on Device ( func stopUpdates() {
// ...
motionManager.stopDeviceMotionUpdates()
startIdleSway() // ⚠️ Always starts idle sway, even on device
}Issue: When all listeners disconnect on a real device, the code stops device motion but immediately starts idle sway. This means the animation never truly stops, which defeats the battery optimization goal. Expected behavior: On device with motion available, stopping updates should stop all animation. Idle sway should only be a fallback for simulator/unavailable motion. Suggested: func stopUpdates() {
guard activeListenerCount > 0 else { return }
activeListenerCount -= 1
guard activeListenerCount == 0 else { return }
motionManager.stopDeviceMotionUpdates()
stopIdleSway() // Stop all animations when no listeners
}5. Missing Test Coverage No unit tests were added for:
Suggested: Add tests for the reference counting logic at minimum, since bugs there could cause battery drain or crashes. ⚡ Performance ConsiderationsPositive:
Concern:
🔒 SecurityNo security concerns identified. CoreMotion access is standard for motion-based features and requires proper privacy string in Info.plist (which should already exist). 📋 Best Practices CheckCLAUDE.md Compliance:
SummaryThis is a well-structured feature with good separation of concerns and thoughtful battery optimization. The main issues are:
The code quality is strong overall and follows project conventions well. With the fixes above, this will be production-ready. |
Add motion-driven rotation to the settings toolbar icon using
|
| activeListenerCount = 0 | ||
|
|
||
| motionManager.stopDeviceMotionUpdates() | ||
| startIdleSway() |
There was a problem hiding this comment.
🟢 Low
Animation/DeviceMotionManager.swift:43 stopUpdates() should call stopIdleSway() (not startIdleSway()) when the listener count drops to zero, so no timer keeps running.
| startIdleSway() | |
| stopIdleSway() |
🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file Convos/Design System/Animation/DeviceMotionManager.swift around line 43:
`stopUpdates()` should call `stopIdleSway()` (not `startIdleSway()`) when the listener count drops to zero, so no timer keeps running.
Summary
The balloon icon in the conversations toolbar now responds to device tilt via CoreMotion. When the user tilts the phone, the balloon smoothly sways following gravity (±30° max). On simulator, an idle sway keeps the animation running for preview purposes.
Implementation
gravity.xvia device motion updates at 30HzTest Plan