diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..ff65457
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,51 @@
+name: Build
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: build-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ swiftpm:
+ name: SwiftPM (build + test)
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+ - name: Setup mise
+ uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1
+
+ - name: Build
+ run: mise run build
+
+ - name: Test
+ run: mise run test
+
+ ios:
+ name: iOS (xcodebuild)
+ runs-on: macos-latest
+
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+ - name: Setup mise
+ uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1
+
+ - name: Install xcodegen
+ run: brew install xcodegen
+
+ - name: Build (Dev scheme)
+ run: mise run ios:build:dev
+
+ - name: Build (Prod scheme)
+ if: github.ref == 'refs/heads/main'
+ run: mise run ios:build:prod
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..5215b99
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,19 @@
+name: Release Please
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ draft:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4
+ with:
+ token: ${{ github.token }}
+ release-type: node
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cc7007b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+# Swift Package Manager
+.build/
+*.swiftpm
+.swiftpm/
+
+# Xcode
+xcuserdata/
+*.xcodeproj
+*.xcworkspace
+!*.xcworkspace/contents.xcworkspacedata
+/*.gcno
+*~.nib
+DerivedData/
+
+# Build artifacts
+*.o
+*.a
+*.dylib
+*.framework
+*.app
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+# macOS
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Temporary files
+*.swp
+*.swo
+*~
+.tmp/
+tmp/
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..3294e39
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "github.copilot.chat.commitMessageGeneration.instructions": [
+ {
+ "text": "Use conventional commit format: type(scope): description."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png
new file mode 100644
index 0000000..461c717
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-120.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-120.png
new file mode 100644
index 0000000..9a5ca11
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-120.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-152.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-152.png
new file mode 100644
index 0000000..bb759b1
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-152.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-167.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-167.png
new file mode 100644
index 0000000..b410849
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-167.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-180.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-180.png
new file mode 100644
index 0000000..bf5b504
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-180.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20.png
new file mode 100644
index 0000000..ee1c8db
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png
new file mode 100644
index 0000000..a5a18b7
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40.png
new file mode 100644
index 0000000..1454157
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-58.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-58.png
new file mode 100644
index 0000000..6359a5e
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-58.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60.png
new file mode 100644
index 0000000..1c61272
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-76.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-76.png
new file mode 100644
index 0000000..ff70197
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-76.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-80.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-80.png
new file mode 100644
index 0000000..c6e4a4f
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-80.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-87.png b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-87.png
new file mode 100644
index 0000000..e53abb0
Binary files /dev/null and b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/AppIcon-87.png differ
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..80500cd
--- /dev/null
+++ b/Apps/iOS/BetterFitApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,116 @@
+{
+ "images": [
+ {
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "2x",
+ "filename": "AppIcon-40.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "3x",
+ "filename": "AppIcon-60.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "2x",
+ "filename": "AppIcon-58.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "3x",
+ "filename": "AppIcon-87.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "2x",
+ "filename": "AppIcon-80.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "3x",
+ "filename": "AppIcon-120.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "2x",
+ "filename": "AppIcon-120.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "3x",
+ "filename": "AppIcon-180.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "20x20",
+ "scale": "1x",
+ "filename": "AppIcon-20.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "20x20",
+ "scale": "2x",
+ "filename": "AppIcon-40.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "29x29",
+ "scale": "1x",
+ "filename": "AppIcon-29.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "29x29",
+ "scale": "2x",
+ "filename": "AppIcon-58.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "40x40",
+ "scale": "1x",
+ "filename": "AppIcon-40.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "40x40",
+ "scale": "2x",
+ "filename": "AppIcon-80.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "76x76",
+ "scale": "1x",
+ "filename": "AppIcon-76.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "76x76",
+ "scale": "2x",
+ "filename": "AppIcon-152.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "83.5x83.5",
+ "scale": "2x",
+ "filename": "AppIcon-167.png"
+ },
+ {
+ "idiom": "ios-marketing",
+ "size": "1024x1024",
+ "scale": "1x",
+ "filename": "AppIcon-1024.png"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
\ No newline at end of file
diff --git a/Apps/iOS/BetterFitApp/Assets.xcassets/Contents.json b/Apps/iOS/BetterFitApp/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..65af1f2
--- /dev/null
+++ b/Apps/iOS/BetterFitApp/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
\ No newline at end of file
diff --git a/Apps/iOS/BetterFitApp/BetterFitApp.swift b/Apps/iOS/BetterFitApp/BetterFitApp.swift
new file mode 100644
index 0000000..655b3b7
--- /dev/null
+++ b/Apps/iOS/BetterFitApp/BetterFitApp.swift
@@ -0,0 +1,13 @@
+import BetterFit
+import SwiftUI
+
+@main
+struct BetterFitApp: App {
+ let betterFit = BetterFit()
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView(betterFit: betterFit)
+ }
+ }
+}
diff --git a/Apps/iOS/BetterFitApp/ContentView.swift b/Apps/iOS/BetterFitApp/ContentView.swift
new file mode 100644
index 0000000..5a55fe9
--- /dev/null
+++ b/Apps/iOS/BetterFitApp/ContentView.swift
@@ -0,0 +1,68 @@
+import BetterFit
+import SwiftUI
+
+struct ContentView: View {
+ let betterFit: BetterFit
+
+ @State private var lastEvent: String = ""
+ @State private var recoveryPercent: Double = 0
+
+ var body: some View {
+ NavigationStack {
+ List {
+ Section("Status") {
+ LabeledContent("Overall recovery") {
+ Text("\(Int(recoveryPercent))%")
+ .monospacedDigit()
+ }
+
+ if !lastEvent.isEmpty {
+ LabeledContent("Last event") {
+ Text(lastEvent)
+ }
+ }
+ }
+
+ Section("Quick actions") {
+ Button("Simulate workout + update recovery") {
+ simulateWorkout()
+ }
+ }
+ }
+ .navigationTitle("BetterFit")
+ }
+ .onAppear {
+ refreshRecovery()
+ }
+ }
+
+ private func refreshRecovery() {
+ recoveryPercent = betterFit.bodyMapManager.getOverallRecoveryPercentage()
+ }
+
+ private func simulateWorkout() {
+ // Minimal "fake" workout flow just to prove the library runs in an app.
+ let benchPress = Exercise(
+ name: "Bench Press",
+ equipmentRequired: .barbell,
+ muscleGroups: [.chest, .triceps]
+ )
+
+ let workout = Workout(
+ name: "Quick Session",
+ exercises: [
+ WorkoutExercise(exercise: benchPress, sets: [ExerciseSet(reps: 8, weight: 60)])
+ ]
+ )
+
+ betterFit.startWorkout(workout)
+ betterFit.completeWorkout(workout)
+
+ lastEvent = "Completed workout"
+ refreshRecovery()
+ }
+}
+
+#Preview {
+ ContentView(betterFit: BetterFit())
+}
diff --git a/Apps/iOS/BetterFitApp/Info.plist b/Apps/iOS/BetterFitApp/Info.plist
new file mode 100644
index 0000000..d243989
--- /dev/null
+++ b/Apps/iOS/BetterFitApp/Info.plist
@@ -0,0 +1,43 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ BetterFit Dev
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+
+
+
+
+
+ UILaunchScreen
+
+
+
diff --git a/Apps/iOS/project.yml b/Apps/iOS/project.yml
new file mode 100644
index 0000000..1fc6659
--- /dev/null
+++ b/Apps/iOS/project.yml
@@ -0,0 +1,74 @@
+name: BetterFit
+options:
+ bundleIdPrefix: dev.echohello
+ deploymentTarget:
+ iOS: "17.0"
+
+packages:
+ BetterFit:
+ path: ../..
+
+targets:
+ BetterFitApp:
+ type: application
+ platform: iOS
+ sources:
+ - path: BetterFitApp
+ resources:
+ - path: BetterFitApp/Assets.xcassets
+ info:
+ path: BetterFitApp/Info.plist
+ properties:
+ CFBundleDisplayName: BetterFit
+ UILaunchScreen: {}
+ UIApplicationSceneManifest:
+ UIApplicationSupportsMultipleScenes: true
+ UISceneConfigurations:
+ UIWindowSceneSessionRoleApplication:
+ - UISceneConfigurationName: Default Configuration
+ UISceneDelegateClassName: ""
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: dev.echohello.BetterFit
+ SWIFT_VERSION: "5.0"
+ dependencies:
+ - package: BetterFit
+
+ BetterFitDev:
+ type: application
+ platform: iOS
+ sources:
+ - path: BetterFitApp
+ resources:
+ - path: BetterFitApp/Assets.xcassets
+ info:
+ path: BetterFitApp/Info.plist
+ properties:
+ CFBundleDisplayName: BetterFit Dev
+ UILaunchScreen: {}
+ UIApplicationSceneManifest:
+ UIApplicationSupportsMultipleScenes: true
+ UISceneConfigurations:
+ UIWindowSceneSessionRoleApplication:
+ - UISceneConfigurationName: Default Configuration
+ UISceneDelegateClassName: ""
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: dev.echohello.BetterFit.dev
+ SWIFT_VERSION: "5.0"
+ dependencies:
+ - package: BetterFit
+
+schemes:
+ BetterFit:
+ build:
+ targets:
+ BetterFitApp: all
+ run:
+ config: Debug
+ BetterFitDev:
+ build:
+ targets:
+ BetterFitDev: all
+ run:
+ config: Debug
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..2655fff
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,65 @@
+# Contributing
+
+## Dev setup
+
+### Prereqs
+
+- Xcode (for iOS Simulator)
+- `mise` (recommended)
+- XcodeGen (only needed if you don’t use the `mise` tasks): `brew install xcodegen`
+
+### Install tools
+
+From the repo root:
+
+```bash
+mise install
+```
+
+### Build + test
+
+```bash
+mise run build
+mise run test
+```
+
+## Run the iOS host app (Simulator)
+
+BetterFit is a Swift Package (library). The runnable iOS host app lives in `Apps/iOS` and is generated via XcodeGen.
+
+### Open in Xcode (recommended)
+
+```bash
+mise run ios:open
+```
+
+In Xcode:
+
+1. Pick a scheme: **BetterFit** (Prod) or **BetterFitDev** (Dev)
+2. Pick an iPhone Simulator
+3. Press **Run**
+
+### Generate the project only
+
+```bash
+mise run ios:gen
+```
+
+### Build from the CLI
+
+```bash
+mise run ios:build:prod
+mise run ios:build:dev
+```
+
+### Simulator troubleshooting
+
+```bash
+mise run ios:sim:reset
+```
+
+## Docs
+
+- High-level entrypoint: `README.md` (root)
+- Reference material lives in `docs/`
+- iOS Simulator run steps live in `docs/README.md`
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..b7b6fa5
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,24 @@
+// swift-tools-version: 5.9
+import PackageDescription
+
+let package = Package(
+ name: "BetterFit",
+ platforms: [
+ .iOS(.v17),
+ .watchOS(.v10)
+ ],
+ products: [
+ .library(
+ name: "BetterFit",
+ targets: ["BetterFit"]),
+ ],
+ dependencies: [],
+ targets: [
+ .target(
+ name: "BetterFit",
+ dependencies: []),
+ .testTarget(
+ name: "BetterFitTests",
+ dependencies: ["BetterFit"]),
+ ]
+)
diff --git a/README.md b/README.md
index 18f5bc9..fac9f58 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,34 @@
-# betterfit
-A workout tracker with auto-tracking capabililty
+# BetterFit
+
+BetterFit is a Swift Package (library) for building a strength training coach experience (iOS + Apple Watch).
+
+## Docs
+
+- [docs/README.md](docs/README.md)
+- [docs/api.md](docs/api.md)
+- [docs/examples.md](docs/examples.md)
+
+## Install (SwiftPM)
+
+```swift
+dependencies: [
+ .package(url: "https://github.com/echohello-dev/betterfit.git", from: "1.0.0")
+]
+```
+
+## Quick usage
+
+```swift
+import BetterFit
+
+let betterFit = BetterFit()
+// Use managers/services, e.g. templates, plans, recovery, auto-tracking
+```
+
+## Development
+
+Dev setup and contributor workflow live in [CONTRIBUTING.md](CONTRIBUTING.md).
+
+## Run on Simulator
+
+See [docs/README.md](docs/README.md) (or [CONTRIBUTING.md](CONTRIBUTING.md)) for the iOS Simulator instructions.
diff --git a/Sources/BetterFit/BetterFit.swift b/Sources/BetterFit/BetterFit.swift
new file mode 100644
index 0000000..f2c8a1e
--- /dev/null
+++ b/Sources/BetterFit/BetterFit.swift
@@ -0,0 +1,149 @@
+import Foundation
+
+/// BetterFit - Open-source strength training coach for iOS and Apple Watch
+///
+/// Core Features:
+/// - Plan mode with AI adaptation
+/// - Reusable workout templates
+/// - Fast equipment swaps
+/// - Body-map recovery view
+/// - Social streaks and challenges
+/// - Smart notifications
+/// - Auto-tracking via Watch sensors
+/// - Clean consistent 3D/AI equipment images
+public class BetterFit {
+
+ // MARK: - Core Services
+
+ public let planManager: PlanManager
+ public let templateManager: TemplateManager
+ public let equipmentSwapManager: EquipmentSwapManager
+ public let bodyMapManager: BodyMapManager
+ public let socialManager: SocialManager
+ public let notificationManager: SmartNotificationManager
+
+ // MARK: - Advanced Services
+
+ public let autoTrackingService: AutoTrackingService
+ public let aiAdaptationService: AIAdaptationService
+ public let imageService: EquipmentImageService
+
+ // MARK: - State
+
+ private var workoutHistory: [Workout] = []
+
+ // MARK: - Initialization
+
+ public init() {
+ self.planManager = PlanManager()
+ self.templateManager = TemplateManager()
+ self.equipmentSwapManager = EquipmentSwapManager()
+ self.bodyMapManager = BodyMapManager()
+ self.socialManager = SocialManager()
+ self.notificationManager = SmartNotificationManager()
+
+ self.autoTrackingService = AutoTrackingService()
+ self.aiAdaptationService = AIAdaptationService()
+ self.imageService = EquipmentImageService()
+ }
+
+ // MARK: - Workout Management
+
+ /// Start a new workout
+ public func startWorkout(_ workout: Workout) {
+ autoTrackingService.startTracking(workout: workout)
+
+ // Schedule smart notifications
+ scheduleWorkoutNotifications()
+ }
+
+ /// Complete a workout
+ public func completeWorkout(_ workout: Workout) {
+ autoTrackingService.stopTracking()
+
+ // Record in history
+ workoutHistory.append(workout)
+
+ // Update recovery map
+ bodyMapManager.recordWorkout(workout)
+
+ // Update streak
+ socialManager.recordWorkout(date: workout.date)
+
+ // Analyze and adapt plan if needed
+ if let activePlan = planManager.getActivePlan() {
+ let adaptations = aiAdaptationService.analyzePerformance(
+ workouts: workoutHistory,
+ currentPlan: activePlan
+ )
+
+ if !adaptations.isEmpty {
+ var updatedPlan = activePlan
+ aiAdaptationService.applyAdaptations(adaptations, to: &updatedPlan)
+ planManager.updatePlan(updatedPlan)
+ }
+ }
+ }
+
+ /// Get workout history
+ public func getWorkoutHistory() -> [Workout] {
+ return workoutHistory
+ }
+
+ // MARK: - Smart Features
+
+ /// Get recommended workout based on recovery and plan
+ public func getRecommendedWorkout() -> Workout? {
+ // Get current plan week
+ guard let activePlan = planManager.getActivePlan(),
+ let currentWeek = activePlan.getCurrentWeek(),
+ let firstWorkoutId = currentWeek.workouts.first else {
+ return nil
+ }
+
+ // Get template for workout
+ guard let template = templateManager.getTemplate(id: firstWorkoutId) else {
+ return nil
+ }
+
+ var workout = template.createWorkout()
+
+ // Check for equipment swaps needed
+ let swaps = equipmentSwapManager.suggestSwaps(for: workout)
+ if !swaps.isEmpty {
+ // Apply first available alternative for each
+ for (original, alternatives) in swaps {
+ if let alternative = alternatives.first {
+ _ = equipmentSwapManager.applySwap(
+ workout: &workout,
+ originalExerciseId: original.id,
+ newExercise: alternative
+ )
+ }
+ }
+ }
+
+ return workout
+ }
+
+ /// Schedule smart notifications
+ private func scheduleWorkoutNotifications() {
+ notificationManager.scheduleNotifications(
+ userProfile: socialManager.getUserProfile(),
+ workoutHistory: workoutHistory,
+ activePlan: planManager.getActivePlan()
+ )
+ }
+
+ // MARK: - Health Integration
+
+ /// Process motion data from Watch
+ public func processMotionData(_ data: MotionData) -> TrackingEvent? {
+ return autoTrackingService.processMotionData(data)
+ }
+
+ /// Get tracking status
+ public func getTrackingStatus() -> TrackingStatus {
+ return autoTrackingService.getTrackingStatus()
+ }
+}
diff --git a/Sources/BetterFit/Features/BodyMap/BodyMapManager.swift b/Sources/BetterFit/Features/BodyMap/BodyMapManager.swift
new file mode 100644
index 0000000..a7c54e5
--- /dev/null
+++ b/Sources/BetterFit/Features/BodyMap/BodyMapManager.swift
@@ -0,0 +1,92 @@
+import Foundation
+
+/// Manages body map recovery view
+public class BodyMapManager {
+ private var recoveryMap: BodyMapRecovery
+
+ public init(recoveryMap: BodyMapRecovery = BodyMapRecovery()) {
+ self.recoveryMap = recoveryMap
+ }
+
+ /// Get current recovery map
+ public func getRecoveryMap() -> BodyMapRecovery {
+ // Update recovery before returning
+ var updatedMap = recoveryMap
+ updatedMap.updateRecovery()
+ recoveryMap = updatedMap
+ return recoveryMap
+ }
+
+ /// Record workout to update recovery map
+ public func recordWorkout(_ workout: Workout) {
+ recoveryMap.recordWorkout(workout)
+ }
+
+ /// Get recovery status for a specific region
+ public func getRecoveryStatus(for region: BodyRegion) -> RecoveryStatus {
+ var updatedMap = recoveryMap
+ updatedMap.updateRecovery()
+ recoveryMap = updatedMap
+
+ return recoveryMap.regions[region] ?? .recovered
+ }
+
+ /// Check if region is ready for training
+ public func isReadyForTraining(region: BodyRegion) -> Bool {
+ let status = getRecoveryStatus(for: region)
+ return status == .recovered || status == .slightlyFatigued
+ }
+
+ /// Get recommended exercises based on recovery status
+ public func getRecommendedExercises(
+ available: [Exercise],
+ avoidSoreRegions: Bool = true
+ ) -> [Exercise] {
+ var updatedMap = recoveryMap
+ updatedMap.updateRecovery()
+
+ return available.filter { exercise in
+ let muscleGroups = exercise.muscleGroups
+ let regions = muscleGroups.map { BodyRegion(rawValue: $0.bodyMapRegion) ?? .other }
+
+ // Check if any targeted region is too sore
+ let hasSoreRegion = regions.contains { region in
+ let status = updatedMap.regions[region] ?? .recovered
+ return status == .sore
+ }
+
+ return !avoidSoreRegions || !hasSoreRegion
+ }
+ }
+
+ /// Get overall recovery percentage
+ public func getOverallRecoveryPercentage() -> Double {
+ var updatedMap = recoveryMap
+ updatedMap.updateRecovery()
+
+ guard !updatedMap.regions.isEmpty else { return 100.0 }
+
+ let totalScore = updatedMap.regions.values.reduce(0.0) { total, status in
+ total + status.recoveryScore
+ }
+
+ return (totalScore / Double(updatedMap.regions.count)) * 100
+ }
+
+ /// Reset recovery map
+ public func reset() {
+ recoveryMap = BodyMapRecovery()
+ }
+}
+
+extension RecoveryStatus {
+ /// Get recovery score (0-1) for overall calculation
+ var recoveryScore: Double {
+ switch self {
+ case .recovered: return 1.0
+ case .slightlyFatigued: return 0.75
+ case .fatigued: return 0.5
+ case .sore: return 0.25
+ }
+ }
+}
diff --git a/Sources/BetterFit/Features/EquipmentSwap/EquipmentSwapManager.swift b/Sources/BetterFit/Features/EquipmentSwap/EquipmentSwapManager.swift
new file mode 100644
index 0000000..1f56dd1
--- /dev/null
+++ b/Sources/BetterFit/Features/EquipmentSwap/EquipmentSwapManager.swift
@@ -0,0 +1,73 @@
+import Foundation
+
+/// Manages fast equipment swaps for available equipment
+public class EquipmentSwapManager {
+ private var availableEquipment: Set
+
+ public init(availableEquipment: Set = Set(Equipment.allCases)) {
+ self.availableEquipment = availableEquipment
+ }
+
+ /// Set available equipment
+ public func setAvailableEquipment(_ equipment: Set) {
+ self.availableEquipment = equipment
+ }
+
+ /// Check if equipment is available
+ public func isAvailable(_ equipment: Equipment) -> Bool {
+ return availableEquipment.contains(equipment)
+ }
+
+ /// Find alternative exercises for unavailable equipment
+ public func findAlternatives(for exercise: Exercise) -> [Exercise] {
+ // If equipment is available, return empty array
+ if isAvailable(exercise.equipmentRequired) {
+ return []
+ }
+
+ // Get alternative equipment options
+ let alternatives = exercise.equipmentRequired.alternatives()
+
+ // Filter to only available equipment
+ let availableAlternatives = alternatives.filter { isAvailable($0) }
+
+ // Create alternative exercises with same muscle groups
+ return availableAlternatives.map { altEquipment in
+ Exercise(
+ name: "\(exercise.name) (\(altEquipment.rawValue))",
+ equipmentRequired: altEquipment,
+ muscleGroups: exercise.muscleGroups,
+ imageURL: exercise.imageURL
+ )
+ }
+ }
+
+ /// Suggest equipment swap for a workout
+ public func suggestSwaps(for workout: Workout) -> [(original: Exercise, alternatives: [Exercise])] {
+ var suggestions: [(Exercise, [Exercise])] = []
+
+ for workoutExercise in workout.exercises {
+ let alternatives = findAlternatives(for: workoutExercise.exercise)
+ if !alternatives.isEmpty {
+ suggestions.append((workoutExercise.exercise, alternatives))
+ }
+ }
+
+ return suggestions
+ }
+
+ /// Apply equipment swap to workout
+ public func applySwap(
+ workout: inout Workout,
+ originalExerciseId: UUID,
+ newExercise: Exercise
+ ) -> Bool {
+ guard let index = workout.exercises.firstIndex(where: { $0.exercise.id == originalExerciseId }) else {
+ return false
+ }
+
+ // Keep the sets but update the exercise
+ workout.exercises[index].exercise = newExercise
+ return true
+ }
+}
diff --git a/Sources/BetterFit/Features/Notifications/SmartNotificationManager.swift b/Sources/BetterFit/Features/Notifications/SmartNotificationManager.swift
new file mode 100644
index 0000000..ee42237
--- /dev/null
+++ b/Sources/BetterFit/Features/Notifications/SmartNotificationManager.swift
@@ -0,0 +1,141 @@
+import Foundation
+
+/// Smart notification manager to minimize gym admin time
+public class SmartNotificationManager {
+ private var scheduledNotifications: [SmartNotification] = []
+
+ public init() {}
+
+ /// Schedule smart notifications based on workout patterns
+ public func scheduleNotifications(
+ userProfile: UserProfile,
+ workoutHistory: [Workout],
+ activePlan: TrainingPlan?
+ ) {
+ scheduledNotifications.removeAll()
+
+ // Workout reminder based on typical workout times
+ if let optimalTime = detectOptimalWorkoutTime(workoutHistory) {
+ let notification = SmartNotification(
+ type: .workoutReminder,
+ scheduledTime: optimalTime,
+ message: "Time for your workout! Let's maintain that \(userProfile.currentStreak)-day streak."
+ )
+ scheduledNotifications.append(notification)
+ }
+
+ // Rest day reminder for overtraining
+ if needsRestDayReminder(workoutHistory) {
+ let notification = SmartNotification(
+ type: .restDayReminder,
+ scheduledTime: Date().addingTimeInterval(3600),
+ message: "Your body needs recovery. Consider taking a rest day."
+ )
+ scheduledNotifications.append(notification)
+ }
+
+ // Plan progress update
+ if let plan = activePlan, let week = plan.getCurrentWeek() {
+ let notification = SmartNotification(
+ type: .planProgress,
+ scheduledTime: Date().addingTimeInterval(86400),
+ message: "Week \(week.weekNumber) complete! Ready for the next challenge?"
+ )
+ scheduledNotifications.append(notification)
+ }
+
+ // Streak maintenance
+ if userProfile.currentStreak > 0 {
+ let notification = SmartNotification(
+ type: .streakMaintenance,
+ scheduledTime: Date().addingTimeInterval(18 * 3600),
+ message: "Don't break your \(userProfile.currentStreak)-day streak! Quick workout?"
+ )
+ scheduledNotifications.append(notification)
+ }
+ }
+
+ /// Detect optimal workout time based on history
+ private func detectOptimalWorkoutTime(_ workouts: [Workout]) -> Date? {
+ guard !workouts.isEmpty else { return nil }
+
+ let calendar = Calendar.current
+ let hourCounts = workouts.reduce(into: [Int: Int]()) { counts, workout in
+ let hour = calendar.component(.hour, from: workout.date)
+ counts[hour, default: 0] += 1
+ }
+
+ // Find most common hour
+ guard let mostCommonHour = hourCounts.max(by: { $0.value < $1.value })?.key else {
+ return nil
+ }
+
+ // Schedule for tomorrow at that hour
+ var components = calendar.dateComponents([.year, .month, .day], from: Date())
+ components.hour = mostCommonHour
+ components.minute = 0
+
+ if let scheduled = calendar.date(from: components),
+ scheduled < Date() {
+ // If time has passed today, schedule for tomorrow
+ return calendar.date(byAdding: .day, value: 1, to: scheduled)
+ }
+
+ return calendar.date(from: components)
+ }
+
+ /// Check if user needs a rest day reminder
+ private func needsRestDayReminder(_ workouts: [Workout]) -> Bool {
+ let recentWorkouts = workouts.filter {
+ $0.date > Date().addingTimeInterval(-7 * 86400)
+ }
+
+ // More than 6 workouts in a week might indicate overtraining
+ return recentWorkouts.count > 6
+ }
+
+ /// Get all scheduled notifications
+ public func getScheduledNotifications() -> [SmartNotification] {
+ return scheduledNotifications.filter { $0.scheduledTime > Date() }
+ }
+
+ /// Cancel a notification
+ public func cancelNotification(id: UUID) {
+ scheduledNotifications.removeAll { $0.id == id }
+ }
+
+ /// Cancel all notifications
+ public func cancelAllNotifications() {
+ scheduledNotifications.removeAll()
+ }
+}
+
+/// Smart notification model
+public struct SmartNotification: Identifiable, Equatable {
+ public let id: UUID
+ public var type: NotificationType
+ public var scheduledTime: Date
+ public var message: String
+
+ public init(
+ id: UUID = UUID(),
+ type: NotificationType,
+ scheduledTime: Date,
+ message: String
+ ) {
+ self.id = id
+ self.type = type
+ self.scheduledTime = scheduledTime
+ self.message = message
+ }
+}
+
+/// Notification types
+public enum NotificationType: String, Codable {
+ case workoutReminder
+ case restDayReminder
+ case planProgress
+ case streakMaintenance
+ case challengeUpdate
+ case recoveryAlert
+}
diff --git a/Sources/BetterFit/Features/PlanMode/PlanManager.swift b/Sources/BetterFit/Features/PlanMode/PlanManager.swift
new file mode 100644
index 0000000..f5641f3
--- /dev/null
+++ b/Sources/BetterFit/Features/PlanMode/PlanManager.swift
@@ -0,0 +1,49 @@
+import Foundation
+
+/// Manages training plans
+public class PlanManager {
+ private var plans: [TrainingPlan]
+ private var activePlanId: UUID?
+
+ public init(plans: [TrainingPlan] = [], activePlanId: UUID? = nil) {
+ self.plans = plans
+ self.activePlanId = activePlanId
+ }
+
+ /// Get the currently active plan
+ public func getActivePlan() -> TrainingPlan? {
+ guard let id = activePlanId else { return nil }
+ return plans.first { $0.id == id }
+ }
+
+ /// Set a plan as active
+ public func setActivePlan(_ planId: UUID) {
+ guard plans.contains(where: { $0.id == planId }) else { return }
+ activePlanId = planId
+ }
+
+ /// Add a new plan
+ public func addPlan(_ plan: TrainingPlan) {
+ plans.append(plan)
+ }
+
+ /// Update an existing plan
+ public func updatePlan(_ plan: TrainingPlan) {
+ if let index = plans.firstIndex(where: { $0.id == plan.id }) {
+ plans[index] = plan
+ }
+ }
+
+ /// Remove a plan
+ public func removePlan(_ planId: UUID) {
+ plans.removeAll { $0.id == planId }
+ if activePlanId == planId {
+ activePlanId = nil
+ }
+ }
+
+ /// Get all plans
+ public func getAllPlans() -> [TrainingPlan] {
+ return plans
+ }
+}
diff --git a/Sources/BetterFit/Features/PlanMode/TrainingPlan.swift b/Sources/BetterFit/Features/PlanMode/TrainingPlan.swift
new file mode 100644
index 0000000..a7350ab
--- /dev/null
+++ b/Sources/BetterFit/Features/PlanMode/TrainingPlan.swift
@@ -0,0 +1,100 @@
+import Foundation
+
+/// Training plan for structured workout programming
+public struct TrainingPlan: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var name: String
+ public var description: String?
+ public var weeks: [TrainingWeek]
+ public var currentWeek: Int
+ public var goal: TrainingGoal
+ public var createdDate: Date
+ public var aiAdapted: Bool
+
+ public init(
+ id: UUID = UUID(),
+ name: String,
+ description: String? = nil,
+ weeks: [TrainingWeek] = [],
+ currentWeek: Int = 0,
+ goal: TrainingGoal,
+ createdDate: Date = Date(),
+ aiAdapted: Bool = false
+ ) {
+ self.id = id
+ self.name = name
+ self.description = description
+ self.weeks = weeks
+ self.currentWeek = currentWeek
+ self.goal = goal
+ self.createdDate = createdDate
+ self.aiAdapted = aiAdapted
+ }
+
+ /// Get the current week's plan
+ public func getCurrentWeek() -> TrainingWeek? {
+ guard currentWeek < weeks.count else { return nil }
+ return weeks[currentWeek]
+ }
+
+ /// Progress to next week
+ public mutating func advanceWeek() {
+ if currentWeek < weeks.count - 1 {
+ currentWeek += 1
+ }
+ }
+}
+
+/// A week in a training plan
+public struct TrainingWeek: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var weekNumber: Int
+ public var workouts: [UUID]
+ public var notes: String?
+
+ public init(
+ id: UUID = UUID(),
+ weekNumber: Int,
+ workouts: [UUID] = [],
+ notes: String? = nil
+ ) {
+ self.id = id
+ self.weekNumber = weekNumber
+ self.workouts = workouts
+ self.notes = notes
+ }
+}
+
+/// Training goals
+public enum TrainingGoal: String, Codable, CaseIterable {
+ case strength
+ case hypertrophy
+ case endurance
+ case powerlifting
+ case generalFitness
+ case weightLoss
+
+ /// Recommended rep ranges for goal
+ public var repRange: ClosedRange {
+ switch self {
+ case .strength: return 1...5
+ case .hypertrophy: return 6...12
+ case .endurance: return 12...20
+ case .powerlifting: return 1...5
+ case .generalFitness: return 8...15
+ case .weightLoss: return 10...20
+ }
+ }
+
+ /// Recommended rest time between sets
+ public var restTime: TimeInterval {
+ switch self {
+ case .strength: return 180
+ case .hypertrophy: return 90
+ case .endurance: return 60
+ case .powerlifting: return 240
+ case .generalFitness: return 90
+ case .weightLoss: return 45
+ }
+ }
+}
diff --git a/Sources/BetterFit/Features/Social/SocialManager.swift b/Sources/BetterFit/Features/Social/SocialManager.swift
new file mode 100644
index 0000000..a9c4429
--- /dev/null
+++ b/Sources/BetterFit/Features/Social/SocialManager.swift
@@ -0,0 +1,140 @@
+import Foundation
+
+/// Manages social features including streaks and challenges
+public class SocialManager {
+ private var userProfile: UserProfile
+ private var challenges: [Challenge]
+ private var streak: Streak
+
+ public init(
+ userProfile: UserProfile = UserProfile(username: "User"),
+ challenges: [Challenge] = [],
+ streak: Streak = Streak()
+ ) {
+ self.userProfile = userProfile
+ self.challenges = challenges
+ self.streak = streak
+ }
+
+ // MARK: - Streak Management
+
+ /// Update streak with completed workout
+ public func recordWorkout(date: Date = Date()) {
+ streak.updateWithWorkout(date: date)
+ userProfile.currentStreak = streak.currentStreak
+ userProfile.longestStreak = max(userProfile.longestStreak, streak.currentStreak)
+ userProfile.totalWorkouts += 1
+ }
+
+ /// Get current streak
+ public func getCurrentStreak() -> Int {
+ return streak.currentStreak
+ }
+
+ /// Get longest streak
+ public func getLongestStreak() -> Int {
+ return streak.longestStreak
+ }
+
+ // MARK: - Challenge Management
+
+ /// Get all challenges
+ public func getAllChallenges() -> [Challenge] {
+ return challenges
+ }
+
+ /// Get active challenges for user
+ public func getActiveChallenges() -> [Challenge] {
+ let now = Date()
+ return challenges.filter { challenge in
+ challenge.startDate <= now &&
+ challenge.endDate >= now &&
+ challenge.participants.contains(userProfile.id)
+ }
+ }
+
+ /// Join a challenge
+ public func joinChallenge(_ challengeId: UUID) -> Bool {
+ guard let index = challenges.firstIndex(where: { $0.id == challengeId }) else {
+ return false
+ }
+
+ if !challenges[index].participants.contains(userProfile.id) {
+ challenges[index].participants.append(userProfile.id)
+ userProfile.activeChallenges.append(challengeId)
+ }
+
+ return true
+ }
+
+ /// Leave a challenge
+ public func leaveChallenge(_ challengeId: UUID) -> Bool {
+ guard let index = challenges.firstIndex(where: { $0.id == challengeId }) else {
+ return false
+ }
+
+ challenges[index].participants.removeAll { $0 == userProfile.id }
+ userProfile.activeChallenges.removeAll { $0 == challengeId }
+
+ return true
+ }
+
+ /// Create a new challenge
+ public func createChallenge(_ challenge: Challenge) {
+ challenges.append(challenge)
+ }
+
+ /// Update challenge progress
+ public func updateChallengeProgress(
+ challengeId: UUID,
+ userId: UUID,
+ progress: Double
+ ) -> Bool {
+ guard let index = challenges.firstIndex(where: { $0.id == challengeId }) else {
+ return false
+ }
+
+ challenges[index].progress[userId] = progress
+ return true
+ }
+
+ /// Get challenge leaderboard
+ public func getChallengeLeaderboard(challengeId: UUID) -> [(userId: UUID, progress: Double)] {
+ guard let challenge = challenges.first(where: { $0.id == challengeId }) else {
+ return []
+ }
+
+ return challenge.progress.sorted { $0.value > $1.value }.map { ($0.key, $0.value) }
+ }
+
+ /// Check if user completed challenge goal
+ public func checkChallengeCompletion(challengeId: UUID, userId: UUID) -> Bool {
+ guard let challenge = challenges.first(where: { $0.id == challengeId }),
+ let progress = challenge.progress[userId] else {
+ return false
+ }
+
+ switch challenge.goal {
+ case .workoutCount(let target):
+ return progress >= Double(target)
+ case .totalVolume(let target):
+ return progress >= target
+ case .consecutiveDays(let target):
+ return progress >= Double(target)
+ case .specificExercise(_, let target):
+ return progress >= Double(target)
+ }
+ }
+
+ // MARK: - User Profile
+
+ /// Get user profile
+ public func getUserProfile() -> UserProfile {
+ return userProfile
+ }
+
+ /// Update user profile
+ public func updateUserProfile(_ profile: UserProfile) {
+ self.userProfile = profile
+ }
+}
diff --git a/Sources/BetterFit/Features/Templates/TemplateManager.swift b/Sources/BetterFit/Features/Templates/TemplateManager.swift
new file mode 100644
index 0000000..7daae73
--- /dev/null
+++ b/Sources/BetterFit/Features/Templates/TemplateManager.swift
@@ -0,0 +1,92 @@
+import Foundation
+
+/// Manages reusable workout templates
+public class TemplateManager {
+ private var templates: [WorkoutTemplate]
+
+ public init(templates: [WorkoutTemplate] = []) {
+ self.templates = templates
+ }
+
+ /// Get all templates
+ public func getAllTemplates() -> [WorkoutTemplate] {
+ return templates
+ }
+
+ /// Get template by ID
+ public func getTemplate(id: UUID) -> WorkoutTemplate? {
+ return templates.first { $0.id == id }
+ }
+
+ /// Add a new template
+ public func addTemplate(_ template: WorkoutTemplate) {
+ templates.append(template)
+ }
+
+ /// Update an existing template
+ public func updateTemplate(_ template: WorkoutTemplate) {
+ if let index = templates.firstIndex(where: { $0.id == template.id }) {
+ templates[index] = template
+ }
+ }
+
+ /// Delete a template
+ public func deleteTemplate(id: UUID) {
+ templates.removeAll { $0.id == id }
+ }
+
+ /// Search templates by tag
+ public func searchByTag(_ tag: String) -> [WorkoutTemplate] {
+ return templates.filter { $0.tags.contains(tag) }
+ }
+
+ /// Search templates by name
+ public func searchByName(_ query: String) -> [WorkoutTemplate] {
+ let lowercasedQuery = query.lowercased()
+ return templates.filter { $0.name.lowercased().contains(lowercasedQuery) }
+ }
+
+ /// Get recently used templates
+ public func getRecentTemplates(limit: Int = 5) -> [WorkoutTemplate] {
+ return templates
+ .filter { $0.lastUsedDate != nil }
+ .sorted { ($0.lastUsedDate ?? .distantPast) > ($1.lastUsedDate ?? .distantPast) }
+ .prefix(limit)
+ .map { $0 }
+ }
+
+ /// Create workout from template
+ public func createWorkout(from templateId: UUID) -> Workout? {
+ guard let template = getTemplate(id: templateId) else {
+ return nil
+ }
+
+ var updatedTemplate = template
+ updatedTemplate.lastUsedDate = Date()
+ updateTemplate(updatedTemplate)
+
+ return template.createWorkout()
+ }
+
+ /// Create template from workout
+ public func createTemplate(from workout: Workout, name: String, tags: [String] = []) -> WorkoutTemplate {
+ let templateExercises = workout.exercises.map { workoutExercise in
+ let targetSets = workoutExercise.sets.map { set in
+ TargetSet(reps: set.reps, weight: set.weight)
+ }
+
+ return TemplateExercise(
+ exercise: workoutExercise.exercise,
+ targetSets: targetSets,
+ restTime: nil
+ )
+ }
+
+ return WorkoutTemplate(
+ name: name,
+ description: nil,
+ exercises: templateExercises,
+ tags: tags
+ )
+ }
+}
diff --git a/Sources/BetterFit/Models/Exercise.swift b/Sources/BetterFit/Models/Exercise.swift
new file mode 100644
index 0000000..8f28852
--- /dev/null
+++ b/Sources/BetterFit/Models/Exercise.swift
@@ -0,0 +1,88 @@
+import Foundation
+
+/// Represents a single exercise in a workout
+public struct Exercise: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var name: String
+ public var equipmentRequired: Equipment
+ public var muscleGroups: [MuscleGroup]
+ public var imageURL: String?
+
+ public init(
+ id: UUID = UUID(),
+ name: String,
+ equipmentRequired: Equipment,
+ muscleGroups: [MuscleGroup],
+ imageURL: String? = nil
+ ) {
+ self.id = id
+ self.name = name
+ self.equipmentRequired = equipmentRequired
+ self.muscleGroups = muscleGroups
+ self.imageURL = imageURL
+ }
+}
+
+/// Equipment types for exercises
+public enum Equipment: String, Codable, CaseIterable {
+ case barbell
+ case dumbbell
+ case kettlebell
+ case machine
+ case cable
+ case bodyweight
+ case bands
+ case other
+
+ /// Get alternative equipment for fast swaps
+ public func alternatives() -> [Equipment] {
+ switch self {
+ case .barbell:
+ return [.dumbbell, .machine]
+ case .dumbbell:
+ return [.barbell, .kettlebell]
+ case .kettlebell:
+ return [.dumbbell]
+ case .machine:
+ return [.barbell, .cable]
+ case .cable:
+ return [.machine, .bands]
+ case .bodyweight:
+ return [.bands]
+ case .bands:
+ return [.cable, .bodyweight]
+ case .other:
+ return []
+ }
+ }
+}
+
+/// Muscle groups targeted by exercises
+public enum MuscleGroup: String, Codable, CaseIterable {
+ case chest
+ case back
+ case shoulders
+ case biceps
+ case triceps
+ case forearms
+ case abs
+ case obliques
+ case quads
+ case hamstrings
+ case glutes
+ case calves
+ case traps
+ case lats
+
+ /// Returns the body map region for recovery tracking
+ public var bodyMapRegion: String {
+ switch self {
+ case .chest: return "chest"
+ case .back, .lats: return "back"
+ case .shoulders, .traps: return "shoulders"
+ case .biceps, .triceps, .forearms: return "arms"
+ case .abs, .obliques: return "core"
+ case .quads, .hamstrings, .glutes, .calves: return "legs"
+ }
+ }
+}
diff --git a/Sources/BetterFit/Models/Recovery.swift b/Sources/BetterFit/Models/Recovery.swift
new file mode 100644
index 0000000..88eb346
--- /dev/null
+++ b/Sources/BetterFit/Models/Recovery.swift
@@ -0,0 +1,97 @@
+import Foundation
+
+/// Body map for tracking recovery
+public struct BodyMapRecovery: Codable, Equatable {
+ public var regions: [BodyRegion: RecoveryStatus]
+ public var lastUpdated: Date
+
+ public init(
+ regions: [BodyRegion: RecoveryStatus] = [:],
+ lastUpdated: Date = Date()
+ ) {
+ self.regions = regions
+ self.lastUpdated = lastUpdated
+ }
+
+ /// Update recovery status after a workout
+ public mutating func recordWorkout(_ workout: Workout) {
+ let muscleGroups = workout.exercises.flatMap { $0.exercise.muscleGroups }
+
+ for group in muscleGroups {
+ let region = BodyRegion(rawValue: group.bodyMapRegion) ?? .other
+ let currentStatus = regions[region] ?? .recovered
+
+ // Mark as worked
+ regions[region] = currentStatus.afterWorkout()
+ }
+
+ lastUpdated = Date()
+ }
+
+ /// Update recovery status based on time elapsed
+ public mutating func updateRecovery() {
+ let now = Date()
+
+ for (region, status) in regions {
+ let hoursSince = now.timeIntervalSince(lastUpdated) / 3600
+ regions[region] = status.afterRecovery(hours: hoursSince)
+ }
+
+ lastUpdated = now
+ }
+}
+
+/// Body regions for recovery tracking
+public enum BodyRegion: String, Codable, CaseIterable {
+ case chest
+ case back
+ case shoulders
+ case arms
+ case core
+ case legs
+ case other
+}
+
+/// Recovery status for muscle groups
+public enum RecoveryStatus: String, Codable, Equatable {
+ case recovered
+ case slightlyFatigued
+ case fatigued
+ case sore
+
+ /// Get status after a workout
+ public func afterWorkout() -> RecoveryStatus {
+ switch self {
+ case .recovered:
+ return .fatigued
+ case .slightlyFatigued:
+ return .sore
+ case .fatigued, .sore:
+ return .sore
+ }
+ }
+
+ /// Get status after recovery time
+ public func afterRecovery(hours: Double) -> RecoveryStatus {
+ switch self {
+ case .recovered:
+ return .recovered
+ case .slightlyFatigued:
+ return hours >= 24 ? .recovered : .slightlyFatigued
+ case .fatigued:
+ return hours >= 48 ? .recovered : (hours >= 24 ? .slightlyFatigued : .fatigued)
+ case .sore:
+ return hours >= 72 ? .recovered : (hours >= 48 ? .fatigued : .sore)
+ }
+ }
+
+ /// Recommended rest before training again
+ public var recommendedRestHours: Double {
+ switch self {
+ case .recovered: return 0
+ case .slightlyFatigued: return 24
+ case .fatigued: return 48
+ case .sore: return 72
+ }
+ }
+}
diff --git a/Sources/BetterFit/Models/Set.swift b/Sources/BetterFit/Models/Set.swift
new file mode 100644
index 0000000..42c9ef6
--- /dev/null
+++ b/Sources/BetterFit/Models/Set.swift
@@ -0,0 +1,27 @@
+import Foundation
+
+/// Represents a single set in an exercise
+public struct ExerciseSet: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var reps: Int
+ public var weight: Double?
+ public var isCompleted: Bool
+ public var timestamp: Date?
+ public var autoTracked: Bool
+
+ public init(
+ id: UUID = UUID(),
+ reps: Int,
+ weight: Double? = nil,
+ isCompleted: Bool = false,
+ timestamp: Date? = nil,
+ autoTracked: Bool = false
+ ) {
+ self.id = id
+ self.reps = reps
+ self.weight = weight
+ self.isCompleted = isCompleted
+ self.timestamp = timestamp
+ self.autoTracked = autoTracked
+ }
+}
diff --git a/Sources/BetterFit/Models/Social.swift b/Sources/BetterFit/Models/Social.swift
new file mode 100644
index 0000000..123315e
--- /dev/null
+++ b/Sources/BetterFit/Models/Social.swift
@@ -0,0 +1,109 @@
+import Foundation
+
+/// User profile for social features
+public struct UserProfile: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var username: String
+ public var currentStreak: Int
+ public var longestStreak: Int
+ public var totalWorkouts: Int
+ public var activeChallenges: [UUID]
+
+ public init(
+ id: UUID = UUID(),
+ username: String,
+ currentStreak: Int = 0,
+ longestStreak: Int = 0,
+ totalWorkouts: Int = 0,
+ activeChallenges: [UUID] = []
+ ) {
+ self.id = id
+ self.username = username
+ self.currentStreak = currentStreak
+ self.longestStreak = longestStreak
+ self.totalWorkouts = totalWorkouts
+ self.activeChallenges = activeChallenges
+ }
+}
+
+/// Workout challenge
+public struct Challenge: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var name: String
+ public var description: String
+ public var goal: ChallengeGoal
+ public var startDate: Date
+ public var endDate: Date
+ public var participants: [UUID]
+ public var progress: [UUID: Double]
+
+ public init(
+ id: UUID = UUID(),
+ name: String,
+ description: String,
+ goal: ChallengeGoal,
+ startDate: Date,
+ endDate: Date,
+ participants: [UUID] = [],
+ progress: [UUID: Double] = [:]
+ ) {
+ self.id = id
+ self.name = name
+ self.description = description
+ self.goal = goal
+ self.startDate = startDate
+ self.endDate = endDate
+ self.participants = participants
+ self.progress = progress
+ }
+}
+
+/// Challenge goal types
+public enum ChallengeGoal: Codable, Equatable {
+ case workoutCount(target: Int)
+ case totalVolume(target: Double)
+ case consecutiveDays(target: Int)
+ case specificExercise(exerciseId: UUID, target: Int)
+}
+
+/// Workout streak tracking
+public struct Streak: Codable, Equatable {
+ public var currentStreak: Int
+ public var longestStreak: Int
+ public var lastWorkoutDate: Date?
+
+ public init(
+ currentStreak: Int = 0,
+ longestStreak: Int = 0,
+ lastWorkoutDate: Date? = nil
+ ) {
+ self.currentStreak = currentStreak
+ self.longestStreak = longestStreak
+ self.lastWorkoutDate = lastWorkoutDate
+ }
+
+ /// Update streak based on workout completion
+ public mutating func updateWithWorkout(date: Date) {
+ guard let lastDate = lastWorkoutDate else {
+ currentStreak = 1
+ longestStreak = max(longestStreak, 1)
+ lastWorkoutDate = date
+ return
+ }
+
+ let calendar = Calendar.current
+ let daysDifference = calendar.dateComponents([.day], from: lastDate, to: date).day ?? 0
+
+ if daysDifference == 1 {
+ // Consecutive day
+ currentStreak += 1
+ longestStreak = max(longestStreak, currentStreak)
+ } else if daysDifference > 1 {
+ // Streak broken
+ currentStreak = 1
+ }
+ // Same day doesn't change streak
+
+ lastWorkoutDate = date
+ }
+}
diff --git a/Sources/BetterFit/Models/Workout.swift b/Sources/BetterFit/Models/Workout.swift
new file mode 100644
index 0000000..97a65c8
--- /dev/null
+++ b/Sources/BetterFit/Models/Workout.swift
@@ -0,0 +1,50 @@
+import Foundation
+
+/// Represents a workout session
+public struct Workout: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var name: String
+ public var exercises: [WorkoutExercise]
+ public var date: Date
+ public var duration: TimeInterval?
+ public var isCompleted: Bool
+ public var templateId: UUID?
+
+ public init(
+ id: UUID = UUID(),
+ name: String,
+ exercises: [WorkoutExercise] = [],
+ date: Date = Date(),
+ duration: TimeInterval? = nil,
+ isCompleted: Bool = false,
+ templateId: UUID? = nil
+ ) {
+ self.id = id
+ self.name = name
+ self.exercises = exercises
+ self.date = date
+ self.duration = duration
+ self.isCompleted = isCompleted
+ self.templateId = templateId
+ }
+}
+
+/// Represents an exercise within a workout with its sets
+public struct WorkoutExercise: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var exercise: Exercise
+ public var sets: [ExerciseSet]
+ public var notes: String?
+
+ public init(
+ id: UUID = UUID(),
+ exercise: Exercise,
+ sets: [ExerciseSet] = [],
+ notes: String? = nil
+ ) {
+ self.id = id
+ self.exercise = exercise
+ self.sets = sets
+ self.notes = notes
+ }
+}
diff --git a/Sources/BetterFit/Models/WorkoutTemplate.swift b/Sources/BetterFit/Models/WorkoutTemplate.swift
new file mode 100644
index 0000000..dc9dcd8
--- /dev/null
+++ b/Sources/BetterFit/Models/WorkoutTemplate.swift
@@ -0,0 +1,79 @@
+import Foundation
+
+/// Reusable workout template
+public struct WorkoutTemplate: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var name: String
+ public var description: String?
+ public var exercises: [TemplateExercise]
+ public var tags: [String]
+ public var createdDate: Date
+ public var lastUsedDate: Date?
+
+ public init(
+ id: UUID = UUID(),
+ name: String,
+ description: String? = nil,
+ exercises: [TemplateExercise] = [],
+ tags: [String] = [],
+ createdDate: Date = Date(),
+ lastUsedDate: Date? = nil
+ ) {
+ self.id = id
+ self.name = name
+ self.description = description
+ self.exercises = exercises
+ self.tags = tags
+ self.createdDate = createdDate
+ self.lastUsedDate = lastUsedDate
+ }
+
+ /// Convert template to a workout
+ public func createWorkout() -> Workout {
+ let workoutExercises = exercises.map { templateExercise in
+ WorkoutExercise(
+ exercise: templateExercise.exercise,
+ sets: templateExercise.targetSets.map { target in
+ ExerciseSet(reps: target.reps, weight: target.weight)
+ }
+ )
+ }
+
+ return Workout(
+ name: name,
+ exercises: workoutExercises,
+ templateId: id
+ )
+ }
+}
+
+/// Exercise definition in a template
+public struct TemplateExercise: Identifiable, Codable, Equatable {
+ public let id: UUID
+ public var exercise: Exercise
+ public var targetSets: [TargetSet]
+ public var restTime: TimeInterval?
+
+ public init(
+ id: UUID = UUID(),
+ exercise: Exercise,
+ targetSets: [TargetSet] = [],
+ restTime: TimeInterval? = nil
+ ) {
+ self.id = id
+ self.exercise = exercise
+ self.targetSets = targetSets
+ self.restTime = restTime
+ }
+}
+
+/// Target set configuration
+public struct TargetSet: Codable, Equatable {
+ public var reps: Int
+ public var weight: Double?
+
+ public init(reps: Int, weight: Double? = nil) {
+ self.reps = reps
+ self.weight = weight
+ }
+}
diff --git a/Sources/BetterFit/Services/AI/AIAdaptationService.swift b/Sources/BetterFit/Services/AI/AIAdaptationService.swift
new file mode 100644
index 0000000..d685940
--- /dev/null
+++ b/Sources/BetterFit/Services/AI/AIAdaptationService.swift
@@ -0,0 +1,143 @@
+import Foundation
+
+/// AI service for adaptive training plan adjustments
+public class AIAdaptationService {
+
+ public init() {}
+
+ /// Analyze workout performance and suggest adaptations
+ public func analyzePerformance(
+ workouts: [Workout],
+ currentPlan: TrainingPlan
+ ) -> [Adaptation] {
+ var adaptations: [Adaptation] = []
+
+ // Analyze completion rate
+ let completionRate = calculateCompletionRate(workouts: workouts)
+ if completionRate < 0.7 {
+ adaptations.append(.reduceVolume(percentage: 15))
+ } else if completionRate > 0.95 {
+ adaptations.append(.increaseVolume(percentage: 10))
+ }
+
+ // Analyze progressive overload
+ let isProgressing = checkProgressiveOverload(workouts: workouts)
+ if !isProgressing {
+ adaptations.append(.adjustIntensity(change: 5))
+ }
+
+ // Check for plateau
+ if detectPlateauPhase(workouts: workouts) {
+ adaptations.append(.deloadWeek)
+ }
+
+ return adaptations
+ }
+
+ /// Calculate workout completion rate
+ private func calculateCompletionRate(workouts: [Workout]) -> Double {
+ guard !workouts.isEmpty else { return 0 }
+ let completedSets = workouts.flatMap { $0.exercises }.flatMap { $0.sets }.filter { $0.isCompleted }.count
+ let totalSets = workouts.flatMap { $0.exercises }.flatMap { $0.sets }.count
+ return totalSets > 0 ? Double(completedSets) / Double(totalSets) : 0
+ }
+
+ /// Check if user is achieving progressive overload
+ private func checkProgressiveOverload(workouts: [Workout]) -> Bool {
+ guard workouts.count >= 2 else { return true }
+
+ // Compare recent workouts
+ let recentWorkouts = Array(workouts.suffix(4))
+ let volumes = recentWorkouts.map { calculateVolume($0) }
+
+ // Check if generally trending upward
+ return volumes.last ?? 0 > volumes.first ?? 0
+ }
+
+ /// Calculate total volume of a workout
+ private func calculateVolume(_ workout: Workout) -> Double {
+ return workout.exercises.reduce(0.0) { total, exercise in
+ let exerciseVolume = exercise.sets.reduce(0.0) { setTotal, set in
+ setTotal + (Double(set.reps) * (set.weight ?? 0))
+ }
+ return total + exerciseVolume
+ }
+ }
+
+ /// Detect if user is in a plateau phase
+ private func detectPlateauPhase(workouts: [Workout]) -> Bool {
+ guard workouts.count >= 4 else { return false }
+
+ let recentWorkouts = Array(workouts.suffix(4))
+ let volumes = recentWorkouts.map { calculateVolume($0) }
+
+ // Check if volumes are stagnant
+ let maxVolume = volumes.max() ?? 0
+ let minVolume = volumes.min() ?? 0
+ let variance = maxVolume - minVolume
+
+ return variance < (maxVolume * 0.05) // Less than 5% variance
+ }
+
+ /// Apply adaptations to a training plan
+ public func applyAdaptations(
+ _ adaptations: [Adaptation],
+ to plan: inout TrainingPlan
+ ) {
+ for adaptation in adaptations {
+ switch adaptation {
+ case .reduceVolume(let percentage):
+ // Reduce sets in plan
+ reducePlanVolume(plan: &plan, by: percentage)
+ case .increaseVolume(let percentage):
+ // Add sets in plan
+ increasePlanVolume(plan: &plan, by: percentage)
+ case .adjustIntensity(let change):
+ // Adjust weights
+ adjustPlanIntensity(plan: &plan, by: change)
+ case .deloadWeek:
+ // Insert deload week
+ insertDeloadWeek(plan: &plan)
+ }
+ }
+
+ plan.aiAdapted = true
+ }
+
+ private func reducePlanVolume(plan: inout TrainingPlan, by percentage: Int) {
+ // Implementation would reduce number of sets
+ }
+
+ private func increasePlanVolume(plan: inout TrainingPlan, by percentage: Int) {
+ // Implementation would add sets
+ }
+
+ private func adjustPlanIntensity(plan: inout TrainingPlan, by change: Int) {
+ // Implementation would adjust weights
+ }
+
+ private func insertDeloadWeek(plan: inout TrainingPlan) {
+ // Implementation would add a lighter week
+ }
+}
+
+/// Training plan adaptation suggestions
+public enum Adaptation: Equatable {
+ case reduceVolume(percentage: Int)
+ case increaseVolume(percentage: Int)
+ case adjustIntensity(change: Int)
+ case deloadWeek
+
+ public var description: String {
+ switch self {
+ case .reduceVolume(let percentage):
+ return "Reduce training volume by \(percentage)%"
+ case .increaseVolume(let percentage):
+ return "Increase training volume by \(percentage)%"
+ case .adjustIntensity(let change):
+ return "Adjust intensity by \(change)%"
+ case .deloadWeek:
+ return "Schedule a deload week"
+ }
+ }
+}
diff --git a/Sources/BetterFit/Services/AutoTracking/AutoTrackingService.swift b/Sources/BetterFit/Services/AutoTracking/AutoTrackingService.swift
new file mode 100644
index 0000000..0be1cde
--- /dev/null
+++ b/Sources/BetterFit/Services/AutoTracking/AutoTrackingService.swift
@@ -0,0 +1,144 @@
+import Foundation
+
+/// Auto-tracking service for Watch sensor data
+public class AutoTrackingService {
+ private var isTracking: Bool = false
+ private var currentWorkout: Workout?
+ private var currentExerciseIndex: Int = 0
+ private var detectedReps: Int = 0
+
+ public init() {}
+
+ /// Start tracking a workout
+ public func startTracking(workout: Workout) {
+ self.currentWorkout = workout
+ self.isTracking = true
+ self.currentExerciseIndex = 0
+ self.detectedReps = 0
+ }
+
+ /// Stop tracking
+ public func stopTracking() {
+ self.isTracking = false
+ self.currentWorkout = nil
+ self.currentExerciseIndex = 0
+ self.detectedReps = 0
+ }
+
+ /// Process motion data from Watch sensors
+ public func processMotionData(_ data: MotionData) -> TrackingEvent? {
+ guard isTracking else { return nil }
+
+ // Detect rep based on motion patterns
+ if data.isRepetitionDetected() {
+ detectedReps += 1
+ return .repDetected(count: detectedReps)
+ }
+
+ // Detect rest period
+ if data.isRestPeriod() {
+ let event = TrackingEvent.setCompleted(reps: detectedReps)
+ detectedReps = 0
+ return event
+ }
+
+ return nil
+ }
+
+ /// Complete current set with auto-tracked data
+ public func completeCurrentSet() -> ExerciseSet? {
+ guard let workout = currentWorkout,
+ currentExerciseIndex < workout.exercises.count else {
+ return nil
+ }
+
+ let set = ExerciseSet(
+ reps: detectedReps,
+ isCompleted: true,
+ timestamp: Date(),
+ autoTracked: true
+ )
+
+ detectedReps = 0
+ return set
+ }
+
+ /// Move to next exercise
+ public func nextExercise() {
+ currentExerciseIndex += 1
+ detectedReps = 0
+ }
+
+ /// Get current tracking status
+ public func getTrackingStatus() -> TrackingStatus {
+ return TrackingStatus(
+ isTracking: isTracking,
+ currentExercise: currentExerciseIndex,
+ detectedReps: detectedReps
+ )
+ }
+}
+
+/// Motion data from Watch sensors
+public struct MotionData {
+ public var acceleration: [Double]
+ public var rotation: [Double]
+ public var heartRate: Double?
+ public var timestamp: Date
+
+ public init(
+ acceleration: [Double],
+ rotation: [Double],
+ heartRate: Double? = nil,
+ timestamp: Date = Date()
+ ) {
+ self.acceleration = acceleration
+ self.rotation = rotation
+ self.heartRate = heartRate
+ self.timestamp = timestamp
+ }
+
+ /// Detect if motion data indicates a repetition
+ public func isRepetitionDetected() -> Bool {
+ // Simplified detection: check for significant acceleration change
+ guard acceleration.count >= 3 else { return false }
+ let magnitude = sqrt(
+ acceleration[0] * acceleration[0] +
+ acceleration[1] * acceleration[1] +
+ acceleration[2] * acceleration[2]
+ )
+ return magnitude > 1.5 // Threshold for rep detection
+ }
+
+ /// Detect if motion data indicates rest period
+ public func isRestPeriod() -> Bool {
+ // Simplified detection: check for minimal movement
+ guard acceleration.count >= 3 else { return false }
+ let magnitude = sqrt(
+ acceleration[0] * acceleration[0] +
+ acceleration[1] * acceleration[1] +
+ acceleration[2] * acceleration[2]
+ )
+ return magnitude < 0.2 // Threshold for rest
+ }
+}
+
+/// Tracking events
+public enum TrackingEvent {
+ case repDetected(count: Int)
+ case setCompleted(reps: Int)
+ case exerciseCompleted
+}
+
+/// Current tracking status
+public struct TrackingStatus {
+ public var isTracking: Bool
+ public var currentExercise: Int
+ public var detectedReps: Int
+
+ public init(isTracking: Bool, currentExercise: Int, detectedReps: Int) {
+ self.isTracking = isTracking
+ self.currentExercise = currentExercise
+ self.detectedReps = detectedReps
+ }
+}
diff --git a/Sources/BetterFit/Services/Images/EquipmentImageService.swift b/Sources/BetterFit/Services/Images/EquipmentImageService.swift
new file mode 100644
index 0000000..0a5a363
--- /dev/null
+++ b/Sources/BetterFit/Services/Images/EquipmentImageService.swift
@@ -0,0 +1,112 @@
+import Foundation
+
+/// Service for managing clean consistent 3D/AI equipment images
+public class EquipmentImageService {
+ private var imageCache: [String: EquipmentImage] = [:]
+
+ public init() {
+ initializeDefaultImages()
+ }
+
+ /// Get image for equipment type
+ public func getImage(for equipment: Equipment) -> EquipmentImage? {
+ return imageCache[equipment.rawValue]
+ }
+
+ /// Get image for exercise
+ public func getImage(for exercise: Exercise) -> EquipmentImage? {
+ if let customURL = exercise.imageURL {
+ return imageCache[customURL]
+ }
+ return getImage(for: exercise.equipmentRequired)
+ }
+
+ /// Cache an image
+ public func cacheImage(_ image: EquipmentImage, for key: String) {
+ imageCache[key] = image
+ }
+
+ /// Get all available images
+ public func getAllImages() -> [EquipmentImage] {
+ return Array(imageCache.values)
+ }
+
+ /// Initialize default equipment images
+ private func initializeDefaultImages() {
+ for equipment in Equipment.allCases {
+ let image = EquipmentImage(
+ id: UUID(),
+ equipmentType: equipment,
+ url: "https://betterfit.app/images/equipment/\(equipment.rawValue).png",
+ is3D: true,
+ isAIGenerated: true
+ )
+ imageCache[equipment.rawValue] = image
+ }
+ }
+
+ /// Load custom image from URL
+ public func loadCustomImage(url: String, for equipment: Equipment) async throws -> EquipmentImage {
+ // In a real implementation, this would fetch the image
+ let image = EquipmentImage(
+ id: UUID(),
+ equipmentType: equipment,
+ url: url,
+ is3D: false,
+ isAIGenerated: false
+ )
+
+ imageCache[url] = image
+ return image
+ }
+
+ /// Generate AI image for custom exercise
+ public func generateAIImage(
+ for exercise: Exercise,
+ style: ImageStyle = .realistic3D
+ ) async throws -> EquipmentImage {
+ // In a real implementation, this would call an AI image generation service
+ let image = EquipmentImage(
+ id: UUID(),
+ equipmentType: exercise.equipmentRequired,
+ url: "https://betterfit.app/images/generated/\(exercise.id).png",
+ is3D: style == .realistic3D,
+ isAIGenerated: true
+ )
+
+ let key = exercise.imageURL ?? exercise.id.uuidString
+ imageCache[key] = image
+ return image
+ }
+}
+
+/// Equipment image model
+public struct EquipmentImage: Identifiable, Equatable {
+ public let id: UUID
+ public var equipmentType: Equipment
+ public var url: String
+ public var is3D: Bool
+ public var isAIGenerated: Bool
+
+ public init(
+ id: UUID = UUID(),
+ equipmentType: Equipment,
+ url: String,
+ is3D: Bool,
+ isAIGenerated: Bool
+ ) {
+ self.id = id
+ self.equipmentType = equipmentType
+ self.url = url
+ self.is3D = is3D
+ self.isAIGenerated = isAIGenerated
+ }
+}
+
+/// Image generation styles
+public enum ImageStyle: String, CaseIterable {
+ case realistic3D
+ case cartoon
+ case schematic
+ case photographic
+}
diff --git a/Tests/BetterFitTests/AIAdaptationTests.swift b/Tests/BetterFitTests/AIAdaptationTests.swift
new file mode 100644
index 0000000..95c4bc2
--- /dev/null
+++ b/Tests/BetterFitTests/AIAdaptationTests.swift
@@ -0,0 +1,85 @@
+import XCTest
+@testable import BetterFit
+
+final class AIAdaptationTests: XCTestCase {
+
+ func testAnalyzePerformanceLowCompletion() {
+ let service = AIAdaptationService()
+
+ let exercise = Exercise(
+ name: "Test",
+ equipmentRequired: .barbell,
+ muscleGroups: [.chest]
+ )
+
+ let incompleteWorkouts = [
+ Workout(
+ name: "W1",
+ exercises: [
+ WorkoutExercise(
+ exercise: exercise,
+ sets: [
+ ExerciseSet(reps: 10, isCompleted: false),
+ ExerciseSet(reps: 10, isCompleted: false)
+ ]
+ )
+ ]
+ )
+ ]
+
+ let plan = TrainingPlan(name: "Test", goal: .strength)
+ let adaptations = service.analyzePerformance(
+ workouts: incompleteWorkouts,
+ currentPlan: plan
+ )
+
+ XCTAssertTrue(adaptations.contains {
+ if case .reduceVolume = $0 { return true }
+ return false
+ })
+ }
+
+ func testAnalyzePerformanceHighCompletion() {
+ let service = AIAdaptationService()
+
+ let exercise = Exercise(
+ name: "Test",
+ equipmentRequired: .barbell,
+ muscleGroups: [.chest]
+ )
+
+ let completeWorkouts = [
+ Workout(
+ name: "W1",
+ exercises: [
+ WorkoutExercise(
+ exercise: exercise,
+ sets: [
+ ExerciseSet(reps: 10, isCompleted: true),
+ ExerciseSet(reps: 10, isCompleted: true)
+ ]
+ )
+ ]
+ )
+ ]
+
+ let plan = TrainingPlan(name: "Test", goal: .strength)
+ let adaptations = service.analyzePerformance(
+ workouts: completeWorkouts,
+ currentPlan: plan
+ )
+
+ XCTAssertTrue(adaptations.contains {
+ if case .increaseVolume = $0 { return true }
+ return false
+ })
+ }
+
+ func testAdaptationDescriptions() {
+ let reduceAdaptation = Adaptation.reduceVolume(percentage: 15)
+ XCTAssertEqual(reduceAdaptation.description, "Reduce training volume by 15%")
+
+ let increaseAdaptation = Adaptation.increaseVolume(percentage: 10)
+ XCTAssertEqual(increaseAdaptation.description, "Increase training volume by 10%")
+ }
+}
diff --git a/Tests/BetterFitTests/AutoTrackingTests.swift b/Tests/BetterFitTests/AutoTrackingTests.swift
new file mode 100644
index 0000000..c74ea17
--- /dev/null
+++ b/Tests/BetterFitTests/AutoTrackingTests.swift
@@ -0,0 +1,60 @@
+import XCTest
+@testable import BetterFit
+
+final class AutoTrackingTests: XCTestCase {
+
+ func testStartTracking() {
+ let service = AutoTrackingService()
+ let workout = Workout(name: "Test Workout")
+
+ service.startTracking(workout: workout)
+
+ let status = service.getTrackingStatus()
+ XCTAssertTrue(status.isTracking)
+ XCTAssertEqual(status.currentExercise, 0)
+ XCTAssertEqual(status.detectedReps, 0)
+ }
+
+ func testStopTracking() {
+ let service = AutoTrackingService()
+ let workout = Workout(name: "Test Workout")
+
+ service.startTracking(workout: workout)
+ service.stopTracking()
+
+ let status = service.getTrackingStatus()
+ XCTAssertFalse(status.isTracking)
+ }
+
+ func testMotionDataRepDetection() {
+ let highAcceleration = MotionData(
+ acceleration: [2.0, 1.5, 1.0],
+ rotation: [0, 0, 0]
+ )
+
+ XCTAssertTrue(highAcceleration.isRepetitionDetected())
+
+ let lowAcceleration = MotionData(
+ acceleration: [0.1, 0.1, 0.1],
+ rotation: [0, 0, 0]
+ )
+
+ XCTAssertFalse(lowAcceleration.isRepetitionDetected())
+ }
+
+ func testMotionDataRestDetection() {
+ let restingData = MotionData(
+ acceleration: [0.05, 0.05, 0.05],
+ rotation: [0, 0, 0]
+ )
+
+ XCTAssertTrue(restingData.isRestPeriod())
+
+ let activeData = MotionData(
+ acceleration: [1.0, 1.0, 1.0],
+ rotation: [0, 0, 0]
+ )
+
+ XCTAssertFalse(activeData.isRestPeriod())
+ }
+}
diff --git a/Tests/BetterFitTests/EquipmentSwapTests.swift b/Tests/BetterFitTests/EquipmentSwapTests.swift
new file mode 100644
index 0000000..1b6a15e
--- /dev/null
+++ b/Tests/BetterFitTests/EquipmentSwapTests.swift
@@ -0,0 +1,60 @@
+import XCTest
+@testable import BetterFit
+
+final class EquipmentSwapTests: XCTestCase {
+
+ func testEquipmentAvailability() {
+ let swapManager = EquipmentSwapManager(
+ availableEquipment: [.dumbbell, .bodyweight]
+ )
+
+ XCTAssertTrue(swapManager.isAvailable(.dumbbell))
+ XCTAssertFalse(swapManager.isAvailable(.barbell))
+ }
+
+ func testFindAlternatives() {
+ let swapManager = EquipmentSwapManager(
+ availableEquipment: [.dumbbell, .bodyweight]
+ )
+
+ let exercise = Exercise(
+ name: "Bench Press",
+ equipmentRequired: .barbell,
+ muscleGroups: [.chest]
+ )
+
+ let alternatives = swapManager.findAlternatives(for: exercise)
+ XCTAssertFalse(alternatives.isEmpty)
+ XCTAssertTrue(alternatives.contains { $0.equipmentRequired == .dumbbell })
+ }
+
+ func testApplySwap() {
+ let swapManager = EquipmentSwapManager()
+
+ let originalExercise = Exercise(
+ name: "Barbell Row",
+ equipmentRequired: .barbell,
+ muscleGroups: [.back]
+ )
+
+ let newExercise = Exercise(
+ name: "Dumbbell Row",
+ equipmentRequired: .dumbbell,
+ muscleGroups: [.back]
+ )
+
+ var workout = Workout(
+ name: "Back Day",
+ exercises: [WorkoutExercise(exercise: originalExercise)]
+ )
+
+ let success = swapManager.applySwap(
+ workout: &workout,
+ originalExerciseId: originalExercise.id,
+ newExercise: newExercise
+ )
+
+ XCTAssertTrue(success)
+ XCTAssertEqual(workout.exercises[0].exercise.equipmentRequired, .dumbbell)
+ }
+}
diff --git a/Tests/BetterFitTests/IntegrationTests.swift b/Tests/BetterFitTests/IntegrationTests.swift
new file mode 100644
index 0000000..d0a2b3c
--- /dev/null
+++ b/Tests/BetterFitTests/IntegrationTests.swift
@@ -0,0 +1,69 @@
+import XCTest
+@testable import BetterFit
+
+final class IntegrationTests: XCTestCase {
+
+ func testBetterFitInitialization() {
+ let betterFit = BetterFit()
+
+ XCTAssertNotNil(betterFit.planManager)
+ XCTAssertNotNil(betterFit.templateManager)
+ XCTAssertNotNil(betterFit.equipmentSwapManager)
+ XCTAssertNotNil(betterFit.bodyMapManager)
+ XCTAssertNotNil(betterFit.socialManager)
+ XCTAssertNotNil(betterFit.notificationManager)
+ XCTAssertNotNil(betterFit.autoTrackingService)
+ XCTAssertNotNil(betterFit.aiAdaptationService)
+ XCTAssertNotNil(betterFit.imageService)
+ }
+
+ func testCompleteWorkoutFlow() {
+ let betterFit = BetterFit()
+
+ let exercise = Exercise(
+ name: "Squat",
+ equipmentRequired: .barbell,
+ muscleGroups: [.quads, .glutes]
+ )
+
+ let set = ExerciseSet(reps: 5, weight: 225.0, isCompleted: true)
+ let workoutExercise = WorkoutExercise(exercise: exercise, sets: [set])
+ var workout = Workout(name: "Leg Day", exercises: [workoutExercise])
+ workout.isCompleted = true
+
+ betterFit.startWorkout(workout)
+ betterFit.completeWorkout(workout)
+
+ let history = betterFit.getWorkoutHistory()
+ XCTAssertEqual(history.count, 1)
+
+ let streak = betterFit.socialManager.getCurrentStreak()
+ XCTAssertEqual(streak, 1)
+ }
+
+ func testTemplateToWorkoutIntegration() {
+ let betterFit = BetterFit()
+
+ let exercise = Exercise(
+ name: "Bench Press",
+ equipmentRequired: .barbell,
+ muscleGroups: [.chest, .triceps]
+ )
+
+ let templateExercise = TemplateExercise(
+ exercise: exercise,
+ targetSets: [TargetSet(reps: 8, weight: 185.0)]
+ )
+
+ let template = WorkoutTemplate(
+ name: "Push Day",
+ exercises: [templateExercise]
+ )
+
+ betterFit.templateManager.addTemplate(template)
+
+ let workout = betterFit.templateManager.createWorkout(from: template.id)
+ XCTAssertNotNil(workout)
+ XCTAssertEqual(workout?.name, "Push Day")
+ }
+}
diff --git a/Tests/BetterFitTests/ModelTests.swift b/Tests/BetterFitTests/ModelTests.swift
new file mode 100644
index 0000000..4a4f4da
--- /dev/null
+++ b/Tests/BetterFitTests/ModelTests.swift
@@ -0,0 +1,90 @@
+import XCTest
+@testable import BetterFit
+
+final class ModelTests: XCTestCase {
+
+ func testExerciseCreation() {
+ let exercise = Exercise(
+ name: "Bench Press",
+ equipmentRequired: .barbell,
+ muscleGroups: [.chest, .triceps]
+ )
+
+ XCTAssertEqual(exercise.name, "Bench Press")
+ XCTAssertEqual(exercise.equipmentRequired, .barbell)
+ XCTAssertEqual(exercise.muscleGroups.count, 2)
+ }
+
+ func testEquipmentAlternatives() {
+ let barbellAlternatives = Equipment.barbell.alternatives()
+ XCTAssertTrue(barbellAlternatives.contains(.dumbbell))
+ XCTAssertTrue(barbellAlternatives.contains(.machine))
+ }
+
+ func testMuscleGroupBodyMapRegion() {
+ XCTAssertEqual(MuscleGroup.chest.bodyMapRegion, "chest")
+ XCTAssertEqual(MuscleGroup.biceps.bodyMapRegion, "arms")
+ XCTAssertEqual(MuscleGroup.quads.bodyMapRegion, "legs")
+ }
+
+ func testExerciseSetCreation() {
+ let set = ExerciseSet(reps: 10, weight: 135.0)
+
+ XCTAssertEqual(set.reps, 10)
+ XCTAssertEqual(set.weight, 135.0)
+ XCTAssertFalse(set.isCompleted)
+ XCTAssertFalse(set.autoTracked)
+ }
+
+ func testWorkoutCreation() {
+ let exercise = Exercise(
+ name: "Squat",
+ equipmentRequired: .barbell,
+ muscleGroups: [.quads, .glutes]
+ )
+
+ let workoutExercise = WorkoutExercise(
+ exercise: exercise,
+ sets: [
+ ExerciseSet(reps: 5, weight: 225.0),
+ ExerciseSet(reps: 5, weight: 225.0)
+ ]
+ )
+
+ let workout = Workout(
+ name: "Leg Day",
+ exercises: [workoutExercise]
+ )
+
+ XCTAssertEqual(workout.name, "Leg Day")
+ XCTAssertEqual(workout.exercises.count, 1)
+ XCTAssertEqual(workout.exercises[0].sets.count, 2)
+ }
+
+ func testTemplateToWorkoutConversion() {
+ let exercise = Exercise(
+ name: "Deadlift",
+ equipmentRequired: .barbell,
+ muscleGroups: [.back, .hamstrings]
+ )
+
+ let templateExercise = TemplateExercise(
+ exercise: exercise,
+ targetSets: [
+ TargetSet(reps: 5, weight: 315.0)
+ ]
+ )
+
+ let template = WorkoutTemplate(
+ name: "Power Day",
+ exercises: [templateExercise]
+ )
+
+ let workout = template.createWorkout()
+
+ XCTAssertEqual(workout.name, "Power Day")
+ XCTAssertEqual(workout.templateId, template.id)
+ XCTAssertEqual(workout.exercises.count, 1)
+ XCTAssertEqual(workout.exercises[0].sets.count, 1)
+ }
+}
diff --git a/Tests/BetterFitTests/PlanModeTests.swift b/Tests/BetterFitTests/PlanModeTests.swift
new file mode 100644
index 0000000..43ed61c
--- /dev/null
+++ b/Tests/BetterFitTests/PlanModeTests.swift
@@ -0,0 +1,58 @@
+import XCTest
+@testable import BetterFit
+
+final class PlanModeTests: XCTestCase {
+
+ func testTrainingPlanCreation() {
+ let plan = TrainingPlan(
+ name: "Beginner Strength",
+ goal: .strength
+ )
+
+ XCTAssertEqual(plan.name, "Beginner Strength")
+ XCTAssertEqual(plan.goal, .strength)
+ XCTAssertEqual(plan.currentWeek, 0)
+ }
+
+ func testTrainingGoalRepRanges() {
+ XCTAssertEqual(TrainingGoal.strength.repRange, 1...5)
+ XCTAssertEqual(TrainingGoal.hypertrophy.repRange, 6...12)
+ XCTAssertEqual(TrainingGoal.endurance.repRange, 12...20)
+ }
+
+ func testPlanManagerActivePlan() {
+ let manager = PlanManager()
+
+ let plan = TrainingPlan(
+ name: "Test Plan",
+ goal: .generalFitness
+ )
+
+ manager.addPlan(plan)
+ manager.setActivePlan(plan.id)
+
+ let activePlan = manager.getActivePlan()
+ XCTAssertNotNil(activePlan)
+ XCTAssertEqual(activePlan?.id, plan.id)
+ }
+
+ func testAdvanceWeek() {
+ var plan = TrainingPlan(
+ name: "Progressive Plan",
+ weeks: [
+ TrainingWeek(weekNumber: 1),
+ TrainingWeek(weekNumber: 2)
+ ],
+ currentWeek: 0,
+ goal: .strength
+ )
+
+ XCTAssertEqual(plan.currentWeek, 0)
+
+ plan.advanceWeek()
+ XCTAssertEqual(plan.currentWeek, 1)
+
+ plan.advanceWeek()
+ XCTAssertEqual(plan.currentWeek, 1) // Should not go beyond last week
+ }
+}
diff --git a/Tests/BetterFitTests/RecoveryTests.swift b/Tests/BetterFitTests/RecoveryTests.swift
new file mode 100644
index 0000000..4e3fde0
--- /dev/null
+++ b/Tests/BetterFitTests/RecoveryTests.swift
@@ -0,0 +1,56 @@
+import XCTest
+@testable import BetterFit
+
+final class RecoveryTests: XCTestCase {
+
+ func testRecoveryStatusProgression() {
+ let recovered = RecoveryStatus.recovered
+ XCTAssertEqual(recovered.afterWorkout(), .fatigued)
+
+ let fatigued = RecoveryStatus.fatigued
+ XCTAssertEqual(fatigued.afterRecovery(hours: 48), .recovered)
+ XCTAssertEqual(fatigued.afterRecovery(hours: 24), .slightlyFatigued)
+ }
+
+ func testBodyMapRecoveryUpdate() {
+ var bodyMap = BodyMapRecovery()
+
+ let exercise = Exercise(
+ name: "Bench Press",
+ equipmentRequired: .barbell,
+ muscleGroups: [.chest, .triceps]
+ )
+
+ let workout = Workout(
+ name: "Chest Day",
+ exercises: [WorkoutExercise(exercise: exercise)]
+ )
+
+ bodyMap.recordWorkout(workout)
+
+ XCTAssertNotNil(bodyMap.regions[.chest])
+ XCTAssertEqual(bodyMap.regions[.chest], .fatigued)
+ }
+
+ func testBodyMapManager() {
+ let manager = BodyMapManager()
+
+ let exercise = Exercise(
+ name: "Squat",
+ equipmentRequired: .barbell,
+ muscleGroups: [.quads, .glutes]
+ )
+
+ let workout = Workout(
+ name: "Leg Day",
+ exercises: [WorkoutExercise(exercise: exercise)]
+ )
+
+ manager.recordWorkout(workout)
+
+ let status = manager.getRecoveryStatus(for: .legs)
+ // Squat works both quads and glutes, which both map to legs region
+ // So it gets hit twice: recovered -> fatigued -> sore
+ XCTAssertEqual(status, .sore)
+ }
+}
diff --git a/Tests/BetterFitTests/SocialTests.swift b/Tests/BetterFitTests/SocialTests.swift
new file mode 100644
index 0000000..005ea25
--- /dev/null
+++ b/Tests/BetterFitTests/SocialTests.swift
@@ -0,0 +1,55 @@
+import XCTest
+@testable import BetterFit
+
+final class SocialTests: XCTestCase {
+
+ func testStreakUpdateConsecutiveDays() {
+ var streak = Streak()
+
+ let today = Date()
+ let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)!
+
+ streak.updateWithWorkout(date: yesterday)
+ XCTAssertEqual(streak.currentStreak, 1)
+
+ streak.updateWithWorkout(date: today)
+ XCTAssertEqual(streak.currentStreak, 2)
+ XCTAssertEqual(streak.longestStreak, 2)
+ }
+
+ func testStreakBreak() {
+ var streak = Streak()
+
+ let today = Date()
+ let threeDaysAgo = Calendar.current.date(byAdding: .day, value: -3, to: today)!
+
+ streak.updateWithWorkout(date: threeDaysAgo)
+ XCTAssertEqual(streak.currentStreak, 1)
+
+ streak.updateWithWorkout(date: today)
+ XCTAssertEqual(streak.currentStreak, 1) // Reset
+ XCTAssertEqual(streak.longestStreak, 1)
+ }
+
+ func testChallengeGoals() {
+ let workoutChallenge = Challenge(
+ name: "30 Day Challenge",
+ description: "Complete 30 workouts",
+ goal: .workoutCount(target: 30),
+ startDate: Date(),
+ endDate: Date().addingTimeInterval(30 * 86400)
+ )
+
+ XCTAssertEqual(workoutChallenge.name, "30 Day Challenge")
+ }
+
+ func testSocialManager() {
+ let manager = SocialManager()
+
+ manager.recordWorkout()
+ XCTAssertEqual(manager.getCurrentStreak(), 1)
+
+ let profile = manager.getUserProfile()
+ XCTAssertEqual(profile.totalWorkouts, 1)
+ }
+}
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..1c211f0
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,50 @@
+# BetterFit Docs
+
+## Run the app (iOS Simulator)
+
+BetterFit is a Swift Package (library). To run something on Simulator, use the iOS host app in `Apps/iOS`.
+
+### Prereqs
+
+- Xcode (Simulator)
+- XcodeGen (`brew install xcodegen`) if you’re not using `mise` tasks
+
+### Open in Xcode (recommended)
+
+From the repo root:
+
+```bash
+mise run ios:open
+```
+
+In Xcode:
+
+1. Pick a scheme: **BetterFit** (Prod) or **BetterFitDev** (Dev)
+2. Pick an iPhone Simulator
+3. Press **Run**
+
+### Generate the Xcode project only
+
+```bash
+mise run ios:gen
+```
+
+### Build from the CLI (no UI)
+
+```bash
+mise run ios:build:prod
+mise run ios:build:dev
+```
+
+### Troubleshooting
+
+- If Simulator is acting up:
+
+```bash
+mise run ios:sim:reset
+```
+
+## Library docs
+
+- [API Reference](api.md)
+- [Usage Examples](examples.md)
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..22e1f67
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,525 @@
+# BetterFit API Reference
+
+Looking to run the iOS app on Simulator? Start here: [README.md](README.md).
+
+## Core Class
+
+### `BetterFit`
+
+The main entry point for the BetterFit library.
+
+```swift
+public class BetterFit
+```
+
+#### Properties
+
+- `planManager: PlanManager` - Manages training plans
+- `templateManager: TemplateManager` - Manages workout templates
+- `equipmentSwapManager: EquipmentSwapManager` - Handles equipment swaps
+- `bodyMapManager: BodyMapManager` - Tracks recovery status
+- `socialManager: SocialManager` - Manages social features
+- `notificationManager: SmartNotificationManager` - Handles notifications
+- `autoTrackingService: AutoTrackingService` - Processes Watch sensor data
+- `aiAdaptationService: AIAdaptationService` - AI-powered plan adaptation
+- `imageService: EquipmentImageService` - Equipment image management
+
+#### Methods
+
+##### `startWorkout(_:)`
+```swift
+public func startWorkout(_ workout: Workout)
+```
+Starts a workout and enables auto-tracking.
+
+##### `completeWorkout(_:)`
+```swift
+public func completeWorkout(_ workout: Workout)
+```
+Completes a workout, updating history, recovery, streaks, and AI analysis.
+
+##### `getWorkoutHistory() -> [Workout]`
+```swift
+public func getWorkoutHistory() -> [Workout]
+```
+Returns all completed workouts.
+
+##### `getRecommendedWorkout() -> Workout?`
+```swift
+public func getRecommendedWorkout() -> Workout?
+```
+Gets a recommended workout based on current plan and recovery status.
+
+##### `processMotionData(_:) -> TrackingEvent?`
+```swift
+public func processMotionData(_ data: MotionData) -> TrackingEvent?
+```
+Processes Apple Watch motion data for auto-tracking.
+
+##### `getTrackingStatus() -> TrackingStatus`
+```swift
+public func getTrackingStatus() -> TrackingStatus
+```
+Returns the current auto-tracking status.
+
+## Models
+
+### `Exercise`
+
+Represents a single exercise.
+
+```swift
+public struct Exercise: Identifiable, Codable, Equatable
+```
+
+#### Properties
+- `id: UUID`
+- `name: String`
+- `equipmentRequired: Equipment`
+- `muscleGroups: [MuscleGroup]`
+- `imageURL: String?`
+
+### `Equipment`
+
+Equipment types for exercises.
+
+```swift
+public enum Equipment: String, Codable, CaseIterable
+```
+
+#### Cases
+- `barbell`, `dumbbell`, `kettlebell`, `machine`, `cable`, `bodyweight`, `bands`, `other`
+
+#### Methods
+- `alternatives() -> [Equipment]` - Returns alternative equipment options
+
+### `MuscleGroup`
+
+Muscle groups targeted by exercises.
+
+```swift
+public enum MuscleGroup: String, Codable, CaseIterable
+```
+
+#### Cases
+- `chest`, `back`, `shoulders`, `biceps`, `triceps`, `forearms`
+- `abs`, `obliques`, `quads`, `hamstrings`, `glutes`, `calves`, `traps`, `lats`
+
+#### Properties
+- `bodyMapRegion: String` - Body map region for recovery tracking
+
+### `ExerciseSet`
+
+Represents a single set in an exercise.
+
+```swift
+public struct ExerciseSet: Identifiable, Codable, Equatable
+```
+
+#### Properties
+- `id: UUID`
+- `reps: Int`
+- `weight: Double?`
+- `isCompleted: Bool`
+- `timestamp: Date?`
+- `autoTracked: Bool`
+
+### `Workout`
+
+Represents a workout session.
+
+```swift
+public struct Workout: Identifiable, Codable, Equatable
+```
+
+#### Properties
+- `id: UUID`
+- `name: String`
+- `exercises: [WorkoutExercise]`
+- `date: Date`
+- `duration: TimeInterval?`
+- `isCompleted: Bool`
+- `templateId: UUID?`
+
+### `WorkoutTemplate`
+
+Reusable workout template.
+
+```swift
+public struct WorkoutTemplate: Identifiable, Codable, Equatable
+```
+
+#### Properties
+- `id: UUID`
+- `name: String`
+- `description: String?`
+- `exercises: [TemplateExercise]`
+- `tags: [String]`
+- `createdDate: Date`
+- `lastUsedDate: Date?`
+
+#### Methods
+- `createWorkout() -> Workout` - Converts template to a workout
+
+### `TrainingPlan`
+
+Training plan for structured programming.
+
+```swift
+public struct TrainingPlan: Identifiable, Codable, Equatable
+```
+
+#### Properties
+- `id: UUID`
+- `name: String`
+- `description: String?`
+- `weeks: [TrainingWeek]`
+- `currentWeek: Int`
+- `goal: TrainingGoal`
+- `createdDate: Date`
+- `aiAdapted: Bool`
+
+#### Methods
+- `getCurrentWeek() -> TrainingWeek?`
+- `advanceWeek()`
+
+### `TrainingGoal`
+
+Training goal types.
+
+```swift
+public enum TrainingGoal: String, Codable, CaseIterable
+```
+
+#### Cases
+- `strength`, `hypertrophy`, `endurance`, `powerlifting`, `generalFitness`, `weightLoss`
+
+#### Properties
+- `repRange: ClosedRange` - Recommended rep range
+- `restTime: TimeInterval` - Recommended rest time
+
+### `BodyMapRecovery`
+
+Body map for tracking recovery.
+
+```swift
+public struct BodyMapRecovery: Codable, Equatable
+```
+
+#### Properties
+- `regions: [BodyRegion: RecoveryStatus]`
+- `lastUpdated: Date`
+
+#### Methods
+- `recordWorkout(_:)` - Update recovery after workout
+- `updateRecovery()` - Update based on time elapsed
+
+### `RecoveryStatus`
+
+Recovery status for muscle groups.
+
+```swift
+public enum RecoveryStatus: String, Codable, Equatable
+```
+
+#### Cases
+- `recovered`, `slightlyFatigued`, `fatigued`, `sore`
+
+#### Properties
+- `recommendedRestHours: Double`
+
+### `UserProfile`
+
+User profile for social features.
+
+```swift
+public struct UserProfile: Identifiable, Codable, Equatable
+```
+
+#### Properties
+- `id: UUID`
+- `username: String`
+- `currentStreak: Int`
+- `longestStreak: Int`
+- `totalWorkouts: Int`
+- `activeChallenges: [UUID]`
+
+### `Challenge`
+
+Workout challenge.
+
+```swift
+public struct Challenge: Identifiable, Codable, Equatable
+```
+
+#### Properties
+- `id: UUID`
+- `name: String`
+- `description: String`
+- `goal: ChallengeGoal`
+- `startDate: Date`
+- `endDate: Date`
+- `participants: [UUID]`
+- `progress: [UUID: Double]`
+
+### `ChallengeGoal`
+
+Challenge goal types.
+
+```swift
+public enum ChallengeGoal: Codable, Equatable
+```
+
+#### Cases
+- `workoutCount(target: Int)`
+- `totalVolume(target: Double)`
+- `consecutiveDays(target: Int)`
+- `specificExercise(exerciseId: UUID, target: Int)`
+
+## Managers
+
+### `PlanManager`
+
+Manages training plans.
+
+```swift
+public class PlanManager
+```
+
+#### Methods
+- `getActivePlan() -> TrainingPlan?`
+- `setActivePlan(_:)`
+- `addPlan(_:)`
+- `updatePlan(_:)`
+- `removePlan(_:)`
+- `getAllPlans() -> [TrainingPlan]`
+
+### `TemplateManager`
+
+Manages workout templates.
+
+```swift
+public class TemplateManager
+```
+
+#### Methods
+- `getAllTemplates() -> [WorkoutTemplate]`
+- `getTemplate(id:) -> WorkoutTemplate?`
+- `addTemplate(_:)`
+- `updateTemplate(_:)`
+- `deleteTemplate(id:)`
+- `searchByTag(_:) -> [WorkoutTemplate]`
+- `searchByName(_:) -> [WorkoutTemplate]`
+- `getRecentTemplates(limit:) -> [WorkoutTemplate]`
+- `createWorkout(from:) -> Workout?`
+- `createTemplate(from:name:tags:) -> WorkoutTemplate`
+
+### `EquipmentSwapManager`
+
+Manages equipment swaps.
+
+```swift
+public class EquipmentSwapManager
+```
+
+#### Methods
+- `setAvailableEquipment(_:)`
+- `isAvailable(_:) -> Bool`
+- `findAlternatives(for:) -> [Exercise]`
+- `suggestSwaps(for:) -> [(original: Exercise, alternatives: [Exercise])]`
+- `applySwap(workout:originalExerciseId:newExercise:) -> Bool`
+
+### `BodyMapManager`
+
+Manages body map recovery.
+
+```swift
+public class BodyMapManager
+```
+
+#### Methods
+- `getRecoveryMap() -> BodyMapRecovery`
+- `recordWorkout(_:)`
+- `getRecoveryStatus(for:) -> RecoveryStatus`
+- `isReadyForTraining(region:) -> Bool`
+- `getRecommendedExercises(available:avoidSoreRegions:) -> [Exercise]`
+- `getOverallRecoveryPercentage() -> Double`
+- `reset()`
+
+### `SocialManager`
+
+Manages social features.
+
+```swift
+public class SocialManager
+```
+
+#### Methods
+- `recordWorkout(date:)`
+- `getCurrentStreak() -> Int`
+- `getLongestStreak() -> Int`
+- `getAllChallenges() -> [Challenge]`
+- `getActiveChallenges() -> [Challenge]`
+- `joinChallenge(_:) -> Bool`
+- `leaveChallenge(_:) -> Bool`
+- `createChallenge(_:)`
+- `updateChallengeProgress(challengeId:userId:progress:) -> Bool`
+- `getChallengeLeaderboard(challengeId:) -> [(userId: UUID, progress: Double)]`
+- `checkChallengeCompletion(challengeId:userId:) -> Bool`
+- `getUserProfile() -> UserProfile`
+- `updateUserProfile(_:)`
+
+### `SmartNotificationManager`
+
+Manages smart notifications.
+
+```swift
+public class SmartNotificationManager
+```
+
+#### Methods
+- `scheduleNotifications(userProfile:workoutHistory:activePlan:)`
+- `getScheduledNotifications() -> [SmartNotification]`
+- `cancelNotification(id:)`
+- `cancelAllNotifications()`
+
+## Services
+
+### `AutoTrackingService`
+
+Auto-tracking service for Watch sensors.
+
+```swift
+public class AutoTrackingService
+```
+
+#### Methods
+- `startTracking(workout:)`
+- `stopTracking()`
+- `processMotionData(_:) -> TrackingEvent?`
+- `completeCurrentSet() -> ExerciseSet?`
+- `nextExercise()`
+- `getTrackingStatus() -> TrackingStatus`
+
+### `MotionData`
+
+Motion data from Watch sensors.
+
+```swift
+public struct MotionData
+```
+
+#### Properties
+- `acceleration: [Double]`
+- `rotation: [Double]`
+- `heartRate: Double?`
+- `timestamp: Date`
+
+#### Initializer
+
+```swift
+public init(
+ acceleration: [Double],
+ rotation: [Double],
+ heartRate: Double? = nil,
+ timestamp: Date = Date()
+)
+```
+
+#### Methods
+- `isRepetitionDetected() -> Bool`
+- `isRestPeriod() -> Bool`
+
+### `TrackingEvent`
+
+Tracking events from auto-tracking.
+
+```swift
+public enum TrackingEvent
+```
+
+#### Cases
+- `repDetected(count: Int)`
+- `setCompleted(reps: Int)`
+- `exerciseCompleted`
+
+### `TrackingStatus`
+
+Current auto-tracking status.
+
+```swift
+public struct TrackingStatus
+```
+
+#### Properties
+- `isTracking: Bool`
+- `currentExercise: Int`
+- `detectedReps: Int`
+
+### `AIAdaptationService`
+
+AI service for adaptive training.
+
+```swift
+public class AIAdaptationService
+```
+
+#### Methods
+- `analyzePerformance(workouts:currentPlan:) -> [Adaptation]`
+- `applyAdaptations(_:to:)`
+
+### `Adaptation`
+
+Training plan adaptation suggestions.
+
+```swift
+public enum Adaptation: Equatable
+```
+
+#### Cases
+- `reduceVolume(percentage: Int)`
+- `increaseVolume(percentage: Int)`
+- `adjustIntensity(change: Int)`
+- `deloadWeek`
+
+### `EquipmentImageService`
+
+Service for equipment images.
+
+```swift
+public class EquipmentImageService
+```
+
+#### Methods
+- `getImage(for: Equipment) -> EquipmentImage?`
+- `getImage(for: Exercise) -> EquipmentImage?`
+- `cacheImage(_:for:)`
+- `getAllImages() -> [EquipmentImage]`
+- `loadCustomImage(url:for:) async throws -> EquipmentImage`
+- `generateAIImage(for:style:) async throws -> EquipmentImage`
+
+### `EquipmentImage`
+
+Equipment image model.
+
+```swift
+public struct EquipmentImage: Identifiable, Equatable
+```
+
+#### Properties
+- `id: UUID`
+- `equipmentType: Equipment`
+- `url: String`
+- `is3D: Bool`
+- `isAIGenerated: Bool`
+
+### `ImageStyle`
+
+Image generation styles.
+
+```swift
+public enum ImageStyle: String, CaseIterable
+```
+
+#### Cases
+- `realistic3D`, `cartoon`, `schematic`, `photographic`
diff --git a/docs/examples.md b/docs/examples.md
new file mode 100644
index 0000000..3642320
--- /dev/null
+++ b/docs/examples.md
@@ -0,0 +1,383 @@
+# BetterFit Usage Examples
+
+Looking to run the iOS app on Simulator? Start here: [README.md](README.md).
+
+This document provides practical examples of using BetterFit in your iOS/watchOS app.
+
+## Quick Start
+
+```swift
+import BetterFit
+
+// Initialize the BetterFit instance
+let betterFit = BetterFit()
+```
+
+## Creating a Workout Template
+
+```swift
+// Define exercises
+let benchPress = Exercise(
+ name: "Bench Press",
+ equipmentRequired: .barbell,
+ muscleGroups: [.chest, .triceps]
+)
+
+let squat = Exercise(
+ name: "Squat",
+ equipmentRequired: .barbell,
+ muscleGroups: [.quads, .glutes, .hamstrings]
+)
+
+// Create template exercises with target sets
+let templateExercises = [
+ TemplateExercise(
+ exercise: benchPress,
+ targetSets: [
+ TargetSet(reps: 8, weight: 185),
+ TargetSet(reps: 8, weight: 185),
+ TargetSet(reps: 8, weight: 185)
+ ],
+ restTime: 90
+ ),
+ TemplateExercise(
+ exercise: squat,
+ targetSets: [
+ TargetSet(reps: 5, weight: 225),
+ TargetSet(reps: 5, weight: 225),
+ TargetSet(reps: 5, weight: 225)
+ ],
+ restTime: 180
+ )
+]
+
+// Create and save the template
+let pushTemplate = WorkoutTemplate(
+ name: "Upper Body Push",
+ description: "Chest and triceps focus",
+ exercises: templateExercises,
+ tags: ["push", "chest", "strength"]
+)
+
+betterFit.templateManager.addTemplate(pushTemplate)
+```
+
+## Creating a Workout from Template
+
+```swift
+// Create workout from template
+let workout = betterFit.templateManager.createWorkout(from: pushTemplate.id)
+
+if let workout = workout {
+ // Start the workout (enables auto-tracking)
+ betterFit.startWorkout(workout)
+}
+```
+
+## Using Auto-Tracking with Apple Watch
+
+```swift
+import CoreMotion
+
+// In your watchOS app, collect motion data
+let motionManager = CMMotionManager()
+motionManager.startDeviceMotionUpdates()
+
+// Process motion data in your workout view
+func processMotionUpdate(_ motion: CMDeviceMotion) {
+ let motionData = MotionData(
+ acceleration: [
+ motion.userAcceleration.x,
+ motion.userAcceleration.y,
+ motion.userAcceleration.z
+ ],
+ rotation: [
+ motion.rotationRate.x,
+ motion.rotationRate.y,
+ motion.rotationRate.z
+ ],
+ heartRate: getCurrentHeartRate()
+ )
+
+ // Process with BetterFit
+ if let event = betterFit.processMotionData(motionData) {
+ switch event {
+ case .repDetected(let count):
+ updateUI(repCount: count)
+ case .setCompleted(let reps):
+ completeSet(reps: reps)
+ case .exerciseCompleted:
+ moveToNextExercise()
+ }
+ }
+}
+```
+
+## Handling Equipment Swaps
+
+```swift
+guard var workout = betterFit.templateManager.createWorkout(from: pushTemplate.id) else {
+ return
+}
+
+// Set available equipment (e.g., at a home gym)
+betterFit.equipmentSwapManager.setAvailableEquipment([
+ .dumbbell,
+ .bodyweight,
+ .bands
+])
+
+// Get swap suggestions for a workout
+let swaps = betterFit.equipmentSwapManager.suggestSwaps(for: workout)
+
+for (original, alternatives) in swaps {
+ print("Replace \(original.name) with:")
+ for alt in alternatives {
+ print(" - \(alt.name)")
+ }
+}
+
+// Apply a swap
+if let firstAlternative = swaps.first?.alternatives.first {
+ betterFit.equipmentSwapManager.applySwap(
+ workout: &workout,
+ originalExerciseId: swaps.first!.original.id,
+ newExercise: firstAlternative
+ )
+}
+```
+
+## Creating a Training Plan
+
+```swift
+// Create training weeks
+let week1 = TrainingWeek(
+ weekNumber: 1,
+ workouts: [pushTemplate.id],
+ notes: "Focus on form"
+)
+
+let week2 = TrainingWeek(
+ weekNumber: 2,
+ workouts: [pushTemplate.id],
+ notes: "Increase intensity by 5%"
+)
+
+// Create the plan
+let plan = TrainingPlan(
+ name: "8-Week Strength Builder",
+ description: "Progressive strength training",
+ weeks: [week1, week2],
+ goal: .strength
+)
+
+// Add to plan manager and set as active
+betterFit.planManager.addPlan(plan)
+betterFit.planManager.setActivePlan(plan.id)
+```
+
+## Completing a Workout and AI Adaptation
+
+```swift
+// Complete the workout
+betterFit.completeWorkout(workout)
+
+// AI automatically analyzes performance and adapts the plan
+// Check what adaptations were suggested
+if let activePlan = betterFit.planManager.getActivePlan() {
+ let adaptations = betterFit.aiAdaptationService.analyzePerformance(
+ workouts: betterFit.getWorkoutHistory(),
+ currentPlan: activePlan
+ )
+
+ for adaptation in adaptations {
+ print("AI Suggestion: \(adaptation.description)")
+ }
+}
+```
+
+## Checking Recovery Status
+
+```swift
+// Check overall recovery
+let overallRecovery = betterFit.bodyMapManager.getOverallRecoveryPercentage()
+print("Overall recovery: \(overallRecovery)%")
+
+// Check specific body regions
+let legStatus = betterFit.bodyMapManager.getRecoveryStatus(for: .legs)
+print("Leg recovery: \(legStatus)")
+
+// Check if ready to train
+let readyForLegs = betterFit.bodyMapManager.isReadyForTraining(region: .legs)
+if readyForLegs {
+ print("Ready for leg day!")
+} else {
+ print("Legs need more recovery time")
+}
+
+// Get recommended exercises based on recovery
+let allExercises = [squat, benchPress, /* ... */]
+let recommended = betterFit.bodyMapManager.getRecommendedExercises(
+ available: allExercises,
+ avoidSoreRegions: true
+)
+```
+
+## Social Features
+
+### Managing Streaks
+
+```swift
+// Record a workout (automatically updates streak)
+betterFit.socialManager.recordWorkout()
+
+// Get current streak
+let currentStreak = betterFit.socialManager.getCurrentStreak()
+print("Current streak: \(currentStreak) days")
+
+// Get longest streak
+let longestStreak = betterFit.socialManager.getLongestStreak()
+print("Longest streak: \(longestStreak) days")
+```
+
+### Creating and Joining Challenges
+
+```swift
+// Create a challenge
+let challenge = Challenge(
+ name: "30 Day Challenge",
+ description: "Complete 30 workouts in 30 days",
+ goal: .workoutCount(target: 30),
+ startDate: Date(),
+ endDate: Date().addingTimeInterval(30 * 86400)
+)
+
+betterFit.socialManager.createChallenge(challenge)
+
+// Join a challenge
+betterFit.socialManager.joinChallenge(challenge.id)
+
+// Update progress
+let userProfile = betterFit.socialManager.getUserProfile()
+
+betterFit.socialManager.updateChallengeProgress(
+ challengeId: challenge.id,
+ userId: userProfile.id,
+ progress: 15 // 15 workouts completed
+)
+
+// Check leaderboard
+let leaderboard = betterFit.socialManager.getChallengeLeaderboard(
+ challengeId: challenge.id
+)
+for (index, entry) in leaderboard.enumerated() {
+ print("\(index + 1). User \(entry.userId): \(entry.progress)")
+}
+```
+
+## Smart Notifications
+
+```swift
+// Schedule smart notifications
+betterFit.notificationManager.scheduleNotifications(
+ userProfile: betterFit.socialManager.getUserProfile(),
+ workoutHistory: betterFit.getWorkoutHistory(),
+ activePlan: betterFit.planManager.getActivePlan()
+)
+
+// Get scheduled notifications
+let scheduled = betterFit.notificationManager.getScheduledNotifications()
+for notification in scheduled {
+ print("\(notification.type): \(notification.message)")
+ print("Scheduled for: \(notification.scheduledTime)")
+}
+
+// Cancel a specific notification
+betterFit.notificationManager.cancelNotification(id: notificationId)
+
+// Cancel all notifications
+betterFit.notificationManager.cancelAllNotifications()
+```
+
+## Equipment Images
+
+```swift
+// Get image for equipment
+if let image = betterFit.imageService.getImage(for: .barbell) {
+ print("Barbell image URL: \(image.url)")
+ print("Is 3D: \(image.is3D)")
+ print("Is AI generated: \(image.isAIGenerated)")
+}
+
+// Get image for an exercise
+if let exerciseImage = betterFit.imageService.getImage(for: benchPress) {
+ // Load image from URL
+ loadImage(from: exerciseImage.url)
+}
+
+// Generate AI image for custom exercise
+Task {
+ do {
+ let aiImage = try await betterFit.imageService.generateAIImage(
+ for: benchPress,
+ style: .realistic3D
+ )
+ print("Generated image: \(aiImage.url)")
+ } catch {
+ print("Failed to generate image: \(error)")
+ }
+}
+```
+
+## Complete Workout Flow Example
+
+```swift
+func performWorkout() {
+ // 1. Get or create workout
+ let workout: Workout
+ if let template = betterFit.templateManager.getRecentTemplates(limit: 1).first {
+ workout = betterFit.templateManager.createWorkout(from: template.id)!
+ } else {
+ // Create a new workout
+ workout = Workout(name: "Quick Session")
+ }
+
+ // 2. Check for equipment swaps
+ var finalWorkout = workout
+ let swaps = betterFit.equipmentSwapManager.suggestSwaps(for: workout)
+ if !swaps.isEmpty {
+ // Apply swaps if needed
+ for (original, alternatives) in swaps {
+ if let alt = alternatives.first {
+ betterFit.equipmentSwapManager.applySwap(
+ workout: &finalWorkout,
+ originalExerciseId: original.id,
+ newExercise: alt
+ )
+ }
+ }
+ }
+
+ // 3. Start workout with auto-tracking
+ betterFit.startWorkout(finalWorkout)
+
+ // 4. During workout: process motion data
+ // (See auto-tracking example above)
+
+ // 5. Complete workout
+ betterFit.completeWorkout(finalWorkout)
+
+ // 6. Check updated streak and recovery
+ print("Streak: \(betterFit.socialManager.getCurrentStreak())")
+ print("Recovery: \(betterFit.bodyMapManager.getOverallRecoveryPercentage())%")
+}
+```
+
+## Best Practices
+
+1. **Initialize Once**: Create a single BetterFit instance and reuse it throughout your app
+2. **Save State**: Persist templates, plans, and user profiles to storage
+3. **Background Updates**: Update recovery status in the background as time passes
+4. **Watch Connectivity**: Use WatchConnectivity framework to sync workout data between iOS and watchOS
+5. **Notification Permissions**: Request notification permissions before scheduling smart notifications
+6. **Motion Permissions**: Request motion and fitness permissions on Apple Watch for auto-tracking
diff --git a/mise.toml b/mise.toml
new file mode 100644
index 0000000..36d1c95
--- /dev/null
+++ b/mise.toml
@@ -0,0 +1,25 @@
+[tasks.test]
+run = "swift test"
+
+[tasks.build]
+run = "swift build"
+
+[tasks."ios:gen"]
+run = "cd Apps/iOS && xcodegen generate"
+
+[tasks."ios:open"]
+run = "cd Apps/iOS && xcodegen generate && open BetterFit.xcodeproj"
+
+[tasks."ios:build:prod"]
+run = "cd Apps/iOS && xcodegen generate && xcodebuild -project BetterFit.xcodeproj -scheme BetterFit -destination 'generic/platform=iOS Simulator' build"
+
+[tasks."ios:build:dev"]
+run = "cd Apps/iOS && xcodegen generate && xcodebuild -project BetterFit.xcodeproj -scheme BetterFitDev -destination 'generic/platform=iOS Simulator' build"
+
+[tasks."ios:sim:reset"]
+run = """
+killall -9 Simulator 2>/dev/null || true
+killall -9 CoreSimulatorService 2>/dev/null || true
+killall -9 com.apple.CoreSimulator.CoreSimulatorService 2>/dev/null || true
+xcrun simctl shutdown all || true
+"""