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/API.md b/API.md new file mode 100644 index 0000000..094fbd8 --- /dev/null +++ b/API.md @@ -0,0 +1,493 @@ +# BetterFit API Reference + +## 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. + +## 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` + +#### Methods +- `isRepetitionDetected() -> Bool` +- `isRestPeriod() -> Bool` + +### `TrackingEvent` + +Tracking events from auto-tracking. + +```swift +public enum TrackingEvent +``` + +#### Cases +- `repDetected(count: Int)` +- `setCompleted(reps: Int)` +- `exerciseCompleted` + +### `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/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..5e736e6 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,376 @@ +# BetterFit Usage Examples + +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 +// 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 { + var updatedWorkout = workout + betterFit.equipmentSwapManager.applySwap( + workout: &updatedWorkout, + 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 +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/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..f75834e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,164 @@ -# betterfit -A workout tracker with auto-tracking capabililty +# BetterFit + +**Open-source strength training coach for iOS and Apple Watch** + +BetterFit is a comprehensive workout tracking application that combines intelligent automation with powerful features to optimize your strength training journey. + +## Core Features + +### 🎯 Plan Mode +- **AI-Adapted Training Plans**: Dynamic workout programming that automatically adjusts based on your performance +- **Progressive Overload Tracking**: AI analyzes your workout history and suggests volume/intensity adjustments +- **Multiple Training Goals**: Strength, hypertrophy, endurance, powerlifting, general fitness, and weight loss +- **Weekly Progression**: Structured training weeks with automatic advancement + +### πŸ“‹ Reusable Workout Templates +- **Template Library**: Create and save workout templates for quick reuse +- **Smart Template Creation**: Convert any completed workout into a reusable template +- **Template Tags**: Organize templates with custom tags (e.g., "Push Day", "Legs", "Full Body") +- **Recent Templates**: Quick access to your most frequently used templates + +### πŸ”„ Fast Equipment Swaps +- **Available Equipment Tracking**: Set which equipment you have access to +- **Automatic Alternatives**: Smart suggestions for alternative exercises when equipment isn't available +- **Muscle Group Matching**: Alternative exercises target the same muscle groups +- **One-Tap Swaps**: Quickly replace exercises in your workout + +### πŸ—ΊοΈ Body-Map Recovery View +- **Visual Recovery Tracking**: Body map showing recovery status for each muscle group region +- **Recovery States**: Recovered, slightly fatigued, fatigued, and sore +- **Time-Based Recovery**: Automatic recovery progression based on time elapsed +- **Smart Exercise Recommendations**: Suggests exercises based on which muscle groups are ready to train + +### πŸ† Social Features +- **Workout Streaks**: Track consecutive workout days and compete with yourself +- **Challenges**: Create and join workout challenges with friends +- **Multiple Challenge Types**: + - Workout count challenges + - Total volume challenges + - Consecutive day challenges + - Specific exercise challenges +- **Leaderboards**: Real-time progress tracking for all challenge participants + +### πŸ”” Smart Notifications +- **Optimal Time Detection**: Learns your typical workout times and reminds you accordingly +- **Streak Maintenance**: Gentle reminders to maintain your workout streak +- **Rest Day Alerts**: Warns when you might be overtraining +- **Plan Progress Updates**: Weekly updates on your training plan progress +- **Minimal Admin Time**: Intelligent notifications that reduce gym planning overhead + +### ⌚ Apple Watch Auto-Tracking +- **Sensor-Based Rep Detection**: Automatically counts reps using Watch accelerometer and gyroscope +- **Set Completion Detection**: Identifies rest periods to automatically complete sets +- **Real-Time Tracking**: Live rep counting during your workout +- **Hands-Free Training**: Focus on your workout, not on manually logging + +### 🎨 Clean Consistent 3D/AI Equipment Images +- **3D Equipment Visualization**: High-quality 3D renders of gym equipment +- **AI-Generated Images**: Consistent visual style across all exercises +- **Equipment Library**: Complete library of barbell, dumbbell, machine, cable, bodyweight, and more +- **Custom Exercise Images**: Support for custom exercise visualizations + +## Technical Implementation + +### Architecture +- **Swift Package Manager**: Modern Swift package with iOS 17+ and watchOS 10+ support +- **Model-Driven Design**: Clean separation between data models and business logic +- **Service Layer**: Modular services for tracking, AI, images, and notifications +- **Feature Modules**: Organized feature-specific implementations + +### Core Components + +#### Models +- `Exercise`: Exercise definitions with equipment and muscle group targeting +- `Workout`: Workout sessions with exercises and sets +- `WorkoutTemplate`: Reusable workout configurations +- `TrainingPlan`: Structured multi-week training programs +- `BodyMapRecovery`: Recovery tracking for body regions +- `UserProfile`, `Challenge`, `Streak`: Social features + +#### Services +- `AutoTrackingService`: Apple Watch sensor data processing +- `AIAdaptationService`: Workout analysis and plan adaptation +- `EquipmentImageService`: 3D/AI image management + +#### Features +- `PlanManager`: Training plan management +- `TemplateManager`: Workout template operations +- `EquipmentSwapManager`: Equipment alternative suggestions +- `BodyMapManager`: Recovery tracking and recommendations +- `SocialManager`: Streaks and challenges +- `SmartNotificationManager`: Intelligent notification scheduling + +## Installation + +Add BetterFit to your Swift project: + +```swift +dependencies: [ + .package(url: "https://github.com/echohello-dev/betterfit.git", from: "1.0.0") +] +``` + +## Usage + +```swift +import BetterFit + +// Initialize BetterFit +let betterFit = BetterFit() + +// Create a workout from a template +if let workout = betterFit.templateManager.createWorkout(from: templateId) { + betterFit.startWorkout(workout) +} + +// Auto-track with Watch sensors +let motionData = MotionData(acceleration: [x, y, z], rotation: [rx, ry, rz]) +if let event = betterFit.processMotionData(motionData) { + switch event { + case .repDetected(let count): + print("Detected rep #\(count)") + case .setCompleted(let reps): + print("Set complete with \(reps) reps") + case .exerciseCompleted: + print("Exercise complete") + } +} + +// Complete workout (automatic streak update, recovery tracking, AI analysis) +betterFit.completeWorkout(workout) + +// Check recovery status +let recoveryStatus = betterFit.bodyMapManager.getRecoveryStatus(for: .legs) +print("Legs recovery: \(recoveryStatus)") + +// Get AI recommendations +if let plan = betterFit.planManager.getActivePlan() { + let adaptations = betterFit.aiAdaptationService.analyzePerformance( + workouts: betterFit.getWorkoutHistory(), + currentPlan: plan + ) +} +``` + +## Building and Testing + +```bash +# Build the package +swift build + +# Run tests +swift test + +# Run specific tests +swift test --filter ModelTests +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +See LICENSE file for details. 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..c357e68 --- /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) + } +}