- Change PostHog from exactVersion to revision (tag 3.62.0) in project.yml
- Add deep cache clean step before xcodegen in CI workflow
- Add ONLY_ACTIVE_ARCH=NO to xcodebuild command
- Wrap PostHog usage in #if canImport(PostHog) for graceful degradation
- Add Package.resolved, .build/, DerivedData/ to .gitignore
- Remove stale SPM caches between CI runs (DerivedData, swiftpm caches, act cache)
Xcode demands -scheme when using -derivedDataPath or -clonedSourcePackagesDirPath.
Since we use -target TabataGo, these flags must be removed.
Changes:
- Removed -derivedDataPath and -clonedSourcePackagesDirPath from build command
- Moved SWIFT_ENABLE_EXPLICIT_MODULES=NO to its own line for clarity
- Updated Package IPA step to search from repo root (find ..) with explicit
TabataGo.app name instead of relying on the old derived data path
xcodegen 2.45.4 does not generate a "TabataGo" scheme (only
RevenueCatUI, RevenueCatUITests, TabataGoUITests). The -scheme flag
therefore fails. Reverting to -target TabataGo as was previously used.
Verified: all .accessoryCorner usages in TabataGoComplication.swift are
properly guarded with #if canImport(WatchKit), so the watch target
transitive dependency build will not surface accessoryCorner issues.
- Remove destructive clean step that causes corruption when dirs are in use
- Add -derivedDataPath to isolate CI build from user Xcode cache
- Add -clonedSourcePackagesDirPath to isolate SPM clones
- Fix Package IPA step to use new isolated derived data path
- Replace #if os(watchOS) with #if canImport(WatchKit) for all .accessoryCorner
usages. When the CI builds with forced iphoneos SDK, os(watchOS) evaluates
to true (target platform is watchOS) but the SDK doesn't have WatchKit, so
accessoryCorner is unavailable. canImport(WatchKit) correctly checks SDK
availability rather than target platform.
- Wrap CornerComplicationView struct and its preview in #if canImport(WatchKit)
with @available(watchOS 10.0, *) annotation.
- Change CI build from -target TabataGo -sdk iphoneos to -scheme TabataGo
(without -sdk). This lets xcodebuild pick the correct SDK per target:
iphoneos for iOS, watchos for watchOS. The scheme already exists in
project.yml.
- Remove all ios-deploy references (brew install, USB setup, device discovery)
- Remove iPhone Preparation section (UDID, libimobiledevice)
- Add Firebase App Distribution setup section:
- Firebase project creation via console.firebase.google.com
- Enable App Distribution, get App ID
- Service account with Firebase App Distribution Admin role
- Base64 encoding instructions for service account key
- Gitea secrets: FIREBASE_APP_ID, FIREBASE_SERVICE_ACCOUNT
- Add testers group 'millian-test' and invite flow
- Add Compilation Error Troubleshooting section:
- xcodegen clearing signing team
- TabataGoWatchWidget needing explicit team ID in project.yml
- General build debugging tips
- Update workflow overview to reflect Firebase flow
- Update secrets table and manual test run command
- Add .health step to onboarding between frequency and ready
- HealthStep with non-blocking permission flow (Not Now skips)
- Remove requestAuthorization() from PlayerViewModel.startWorkout()
- Guard live session start with isAuthorized check
- Remove heart rate write from HealthKit authorization popup
- Remove HR sample writing from saveWorkout (now without permission)
- Add L10n keys: healthAccess, healthAccessSubtitle, allowHealthAccess, notNow
- Add EN/DE/ES/FR translations
- Track permission decisions through analytics
- Entry animation on HealthStep (fade-in + slide-up)
HealthKit permission is now asked once during onboarding,
never interrupting workouts again.
Root cause: observeActivityState() prematurely set workoutActivity=nil
when the activity went .stale (e.g. app backgrounded >2 minutes). This
prevented endActivity() from calling .end() on the stale activity,
leaving it visible on the Lock Screen and Dynamic Island indefinitely.
Fixes (all in PlayerViewModel.swift):
1. observeActivityState(): Split the monolithic stale/ended/dismissed
handler. .stale now only stops the sync timer but keeps the
workoutActivity reference so endActivity() can still call .end()
to properly dismiss the stale Live Activity.
2. syncActivity() nil guard: Changed from != .active to explicit
== .ended || == .dismissed so stale activities are not prematurely
discarded when tick() re-enters syncActivity().
3. endActivity(): Added stopActivitySyncTimer() defensive call at top
to prevent orphaned timer from racing in and recreating the activity
during .end(). Also relaxed the guard from == .active to
!= .ended && != .dismissed so stale activities can be ended.
4. abandonWorkout(): Explicitly set isRunning=false + isPaused=false
before cleanup to prevent accidental Live Activity recreation.
- Add CountdownRing with real-time arc progress on lock screen
- Replace generic dots with phase-specific SF Symbols (flame, snowflake, etc.)
- Remove horizontal progress bar in favor of round counter text
- Increase Dynamic Island expanded font sizes for better visibility
- Increase live activity sync frequency from 5s to 1s for smoother arc updates
- Add pause/resume button via TogglePauseIntent AppIntent
- Remove AlertConfiguration to silence notification sounds on updates
- Replace raw string phase model with WorkoutPhase enum (Codable, Sendable, CaseIterable)
with built-in .capitalized display name and SwiftUI .color per phase
- Decompose WorkoutLiveActivity into reusable view structs: PhasePill, CountdownText,
WorkoutProgressBar, MusicInfoRow, HeartRateBadge, PhaseIndicatorDot, WorkoutLockScreenView,
WorkoutSmallView — following CraftingSwift iOS 26 architecture patterns
- Add AlertConfiguration on work/rest/complete phase transitions so Dynamic Island
expands and lights up at key moments
- Add 13 #Preview blocks across both widgets covering all presentation types:
lock screen, expanded, compact, minimal — for instant Xcode Canvas feedback
- Add stale state handling (context.isStale shows 'Last updated' indicator)
- MusicLiveActivity: 5 new #Preview blocks for playing/paused/expanded/compact/minimal
- Add isPaused to WorkoutActivityAttributes.ContentState
- Show PAUSED badge, freeze timer to static text, dim content when paused
- Prevent stale spinner on pause by extending staleDate to 1 hour
- Add 6s timer warning color, progress bar, compact heavy timer
- Pulsing compact indicator during WORK phase
- Lock Screen margins aligned to Apple's 14pt HIG spec
When airplane mode is active, the Supabase client hung indefinitely
waiting for a network response, blocking the mock track fallback.
Now races the query against a 6-second Task.sleep so mock tracks
load immediately after timeout.
- Add @Environment activityFamily, isActivityFullscreen, isLuminanceReduced
- Split into lockScreenView() and smallLockScreenView() variants
- Add supplementalActivityFamilies([.small, .medium]) support
- Add keylineTint and contentMargins to Dynamic Island
- Add accessibility labels throughout (VoiceOver support)
- Hide music bar animation when isLuminanceReduced