Files
WerkoutIOS/iphone/Werkout_ios/DataStore.swift
Trey t 5d39dcb66f
Some checks failed
Apple Platform CI / smoke-and-tests (push) Has been cancelled
Fix 28 issues from deep audit and UI audit + redesign changes
Deep audit (issues 2-14):
- Add missing WCSession handlers for applicationContext and userInfo
- Fix BoundedFIFOQueue race condition with serial dispatch queue
- Fix timer race condition with main thread guarantee
- Fix watch pause state divergence — phone is now source of truth
- Fix wrong notification posted on logout (createdNewWorkout → userLoggedOut)
- Fix POST status check to accept any 2xx (was exact match)
- Fix @StateObject → @ObservedObject for injected viewModel
- Add pull-to-refresh to CompletedWorkoutsView
- Fix typos: RefreshUserInfoFetcable, defualtPackageModle
- Replace string concatenation with interpolation
- Replace 6 @StateObject with @ObservedObject for BridgeModule.shared
- Replace 7 hardcoded AVPlayer URLs with BaseURLs.currentBaseURL

UI audit (issues 1-15):
- Fix GeometryReader eating VStack space — replaced with .overlay
- Fix refreshable continuation resuming before fetch completes
- Remove duplicate @State workouts — derive from DataStore
- Decouple leaf views from BridgeModule (pass discrete values)
- Convert selectedIds from Array to Set for O(1) lookups
- Extract .sorted() from var body into computed properties
- Move search filter out of ForEach render loop
- Replace import SwiftUI with import Combine in non-UI classes
- Mark all @State properties private
- Extract L/R exercise auto-add logic to WorkoutViewModel
- Use enumerated() instead of .indices in ForEach
- Make AddSupersetView frame flexible instead of fixed 300pt
- Hoist Set construction out of per-exercise filter loop
- Move ViewModel network fetch from init to load()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:24:52 -06:00

144 lines
4.7 KiB
Swift

//
// DataStore.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/19/23.
//
import Foundation
import Combine
import SharedCore
class DataStore: ObservableObject {
enum DataStoreStatus {
case loading
case idle
}
static let shared = DataStore()
private let runtimeReporter = RuntimeReporter.shared
public private(set) var allWorkouts: [Workout]?
public private(set) var allMuscles: [Muscle]?
public private(set) var allEquipment: [Equipment]?
public private(set) var allExercise: [Exercise]?
public private(set) var allNSFWVideos: [NSFWVideo]?
@Published public private(set) var status = DataStoreStatus.idle
private var pendingFetchCompletions = [() -> Void]()
public func randomVideoFor(gender: String) -> String? {
return allNSFWVideos?.filter({
$0.genderValue.lowercased() == gender.lowercased()
}).randomElement()?.videoFile ?? nil
}
public var nsfwGenderOptions: [String]? {
let values = self.allNSFWVideos?.map({
$0.genderValue
})
if let values = values {
return Array(Set(values))
}
return nil
}
public var workoutsUniqueUsers: [RegisteredUser]? {
guard let workouts = allWorkouts else {
return nil
}
let users = workouts.compactMap({ $0.registeredUser })
return Array(Set(users))
}
public func fetchAllData(completion: @escaping (() -> Void)) {
if status == .loading {
pendingFetchCompletions.append(completion)
runtimeReporter.recordInfo("fetchAllData called while already loading")
return
}
pendingFetchCompletions = [completion]
status = .loading
let fetchAllDataQueue = DispatchGroup()
fetchAllDataQueue.enter()
fetchAllDataQueue.enter()
fetchAllDataQueue.enter()
fetchAllDataQueue.enter()
fetchAllDataQueue.enter()
fetchAllDataQueue.notify(queue: .main) {
self.status = .idle
let completions = self.pendingFetchCompletions
self.pendingFetchCompletions.removeAll()
completions.forEach { $0() }
}
AllWorkoutFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allWorkouts = model
case .failure(let error):
self.runtimeReporter.recordError("Failed to fetch workouts", metadata: ["error": error.localizedDescription])
}
fetchAllDataQueue.leave()
})
AllMusclesFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allMuscles = model.sorted(by: {
$0.name < $1.name
})
case .failure(let error):
self.runtimeReporter.recordError("Failed to fetch muscles", metadata: ["error": error.localizedDescription])
}
fetchAllDataQueue.leave()
})
AllEquipmentFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allEquipment = model.sorted(by: {
$0.name < $1.name
})
case .failure(let error):
self.runtimeReporter.recordError("Failed to fetch equipment", metadata: ["error": error.localizedDescription])
}
fetchAllDataQueue.leave()
})
AllExerciseFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allExercise = model.sorted(by: {
$0.name < $1.name
})
case .failure(let error):
self.runtimeReporter.recordError("Failed to fetch exercises", metadata: ["error": error.localizedDescription])
}
fetchAllDataQueue.leave()
})
AllNSFWVideosFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allNSFWVideos = model
case .failure(let error):
self.runtimeReporter.recordError("Failed to fetch NSFW videos", metadata: ["error": error.localizedDescription])
}
fetchAllDataQueue.leave()
})
}
func setupFakeData() {
allWorkouts = PreviewData.allWorkouts()
allMuscles = PreviewData.parseMuscle()
allEquipment = PreviewData.parseEquipment()
allExercise = PreviewData.parseExercises()
}
}