Fix 28 issues from deep audit and UI audit + redesign changes
Some checks failed
Apple Platform CI / smoke-and-tests (push) Has been cancelled

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>
This commit is contained in:
Trey t
2026-02-23 10:24:06 -06:00
parent 921829f2c3
commit 5d39dcb66f
60 changed files with 1783 additions and 1346 deletions

View File

@@ -13,88 +13,142 @@ import SharedCore
enum MainViewTypes: Int, CaseIterable {
case AllWorkout = 0
case MyWorkouts
var title: String {
switch self {
case .AllWorkout:
return "All Workouts"
case .MyWorkouts:
return "Planned Workouts"
return "Planned"
}
}
}
struct AllWorkoutsView: View {
@State var isUpdating = false
@State var workouts: [Workout]?
@State var uniqueWorkoutUsers: [RegisteredUser]?
@State private var isUpdating = false
private var workouts: [Workout]? {
dataStore.allWorkouts?.sorted(by: { $0.createdAt ?? Date() < $1.createdAt ?? Date() })
}
private var uniqueWorkoutUsers: [RegisteredUser]? {
dataStore.workoutsUniqueUsers
}
let healthStore = HKHealthStore()
var bridgeModule = BridgeModule.shared
@State public var needsUpdating: Bool = true
@State private var needsUpdating: Bool = true
@ObservedObject var dataStore = DataStore.shared
@ObservedObject var userStore = UserStore.shared
@State private var showWorkoutDetail = false
@State private var selectedWorkout: Workout? {
didSet {
bridgeModule.currentWorkoutInfo.workout = selectedWorkout
}
}
@State private var selectedPlannedWorkout: Workout? {
didSet {
bridgeModule.currentWorkoutInfo.workout = selectedPlannedWorkout
}
}
@State private var showLoginView = false
@State private var selectedSegment: MainViewTypes = .AllWorkout
@State var selectedDate: Date = Date()
@State private var selectedDate: Date = Date()
private let runtimeReporter = RuntimeReporter.shared
// MARK: - Hoisted filter state
@State private var searchText = ""
@State private var selectedMuscles: Set<String> = []
@State private var selectedEquipment: Set<String> = []
@State private var filteredRegisterdUser: RegisteredUser?
@State private var currentSort: SortType?
@State private var sortAscending: Bool = true
let pub = NotificationCenter.default.publisher(for: AppNotifications.createdNewWorkout)
var body: some View {
ZStack {
if let workouts = workouts {
VStack {
AllWorkoutPickerView(mainViews: MainViewTypes.allCases,
selectedSegment: $selectedSegment,
showCurrentWorkout: {
selectedWorkout = bridgeModule.currentWorkoutInfo.workout
})
switch selectedSegment {
case .AllWorkout:
if isUpdating {
ProgressView()
.progressViewStyle(.circular)
}
AllWorkoutsListView(uniqueWorkoutUsers: $uniqueWorkoutUsers,
workouts: workouts,
selectedWorkout: { workout in
selectedWorkout = workout
}, refresh: {
self.needsUpdating = true
maybeRefreshData()
})
private var hasActiveFilters: Bool {
!selectedMuscles.isEmpty || !selectedEquipment.isEmpty || filteredRegisterdUser != nil
}
Divider()
case .MyWorkouts:
PlannedWorkoutView(workouts: userStore.plannedWorkouts,
selectedPlannedWorkout: $selectedPlannedWorkout)
var body: some View {
NavigationStack {
ZStack {
WerkoutTheme.background.ignoresSafeArea()
if let workouts = workouts {
VStack(spacing: 0) {
// Active filter chips
if hasActiveFilters {
filterChipsRow
}
switch selectedSegment {
case .AllWorkout:
if isUpdating {
ProgressView()
.progressViewStyle(.circular)
.tint(WerkoutTheme.accent)
}
AllWorkoutsListView(workouts: workouts,
searchText: $searchText,
selectedMuscles: $selectedMuscles,
selectedEquipment: $selectedEquipment,
filteredRegisterdUser: $filteredRegisterdUser,
currentSort: $currentSort,
sortAscending: $sortAscending,
selectedWorkout: { workout in
selectedWorkout = workout
}, refresh: {
self.needsUpdating = true
maybeRefreshData()
})
case .MyWorkouts:
PlannedWorkoutView(workouts: userStore.plannedWorkouts,
selectedPlannedWorkout: $selectedPlannedWorkout)
}
}
} else {
ProgressView()
.progressViewStyle(.circular)
.tint(WerkoutTheme.accent)
}
}
.navigationTitle("Workouts")
.searchable(text: $searchText, prompt: "Search workouts")
.toolbar {
ToolbarItem(placement: .principal) {
Picker("View", selection: $selectedSegment) {
ForEach(MainViewTypes.allCases, id: \.self) { viewType in
Text(viewType.title).tag(viewType)
}
}
.pickerStyle(.segmented)
.frame(width: 200)
}
ToolbarItem(placement: .topBarTrailing) {
HStack(spacing: WerkoutTheme.sm) {
if bridgeModule.isInWorkout {
Button(action: {
selectedWorkout = bridgeModule.currentWorkoutInfo.workout
}) {
Image(systemName: "figure.strengthtraining.traditional")
}
.tint(WerkoutTheme.accent)
}
filterMenu
}
}
} else {
ProgressView("Updating")
}
}
.background(Color(uiColor: .systemGray5))
.onAppear{
// UserStore.shared.logout()
.onAppear {
authorizeHealthKit()
maybeRefreshData()
}
@@ -114,31 +168,171 @@ struct AllWorkoutsView: View {
})
.interactiveDismissDisabled()
}
.onReceive(pub) { (output) in
.onReceive(pub) { _ in
self.needsUpdating = true
maybeRefreshData()
}
}
// MARK: - Filter Menu
private var filterMenu: some View {
Menu {
// Sort section
Section("Sort") {
ForEach(SortType.allCases, id: \.self) { sortType in
Button(action: {
if currentSort == sortType {
sortAscending.toggle()
} else {
currentSort = sortType
sortAscending = true
}
}) {
Label {
Text(sortType.rawValue)
} icon: {
if currentSort == sortType {
Image(systemName: sortAscending ? "chevron.up" : "chevron.down")
}
}
}
}
}
// Creator filter
if let users = uniqueWorkoutUsers, !users.isEmpty {
Section("Creator") {
ForEach(users, id: \.self) { user in
Button(action: {
filteredRegisterdUser = user
}) {
Label {
Text("\(user.firstName ?? "") \(user.lastName ?? "")")
} icon: {
if filteredRegisterdUser == user {
Image(systemName: "checkmark")
}
}
}
}
Button("All Creators") {
filteredRegisterdUser = nil
}
}
}
// Muscles filter
if let muscles = DataStore.shared.allMuscles {
Section("Muscles") {
ForEach(muscles, id: \.id) { muscle in
Button(action: {
if selectedMuscles.contains(muscle.name) {
selectedMuscles.remove(muscle.name)
} else {
selectedMuscles.insert(muscle.name)
}
}) {
Label {
Text(muscle.name)
} icon: {
if selectedMuscles.contains(muscle.name) {
Image(systemName: "checkmark")
}
}
}
}
}
}
// Equipment filter
if let equipments = DataStore.shared.allEquipment {
Section("Equipment") {
ForEach(equipments, id: \.id) { equipment in
Button(action: {
if selectedEquipment.contains(equipment.name) {
selectedEquipment.remove(equipment.name)
} else {
selectedEquipment.insert(equipment.name)
}
}) {
Label {
Text(equipment.name)
} icon: {
if selectedEquipment.contains(equipment.name) {
Image(systemName: "checkmark")
}
}
}
}
}
}
// Clear all
if hasActiveFilters || currentSort != nil {
Divider()
Button(role: .destructive) {
clearAllFilters()
} label: {
Label("Clear All Filters", systemImage: "xmark.circle")
}
}
} label: {
Image(systemName: hasActiveFilters ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
}
.tint(hasActiveFilters ? WerkoutTheme.accent : nil)
}
// MARK: - Filter Chips Row
private var filterChipsRow: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: WerkoutTheme.sm) {
ForEach(Array(selectedMuscles), id: \.self) { muscle in
FilterChip(label: muscle, color: WerkoutTheme.accent) {
selectedMuscles.remove(muscle)
}
}
ForEach(Array(selectedEquipment), id: \.self) { equipment in
FilterChip(label: equipment, color: WerkoutTheme.textSecondary) {
selectedEquipment.remove(equipment)
}
}
if let user = filteredRegisterdUser {
FilterChip(label: user.firstName ?? "User", color: WerkoutTheme.accentNeon) {
filteredRegisterdUser = nil
}
}
}
.padding(.horizontal, WerkoutTheme.md)
.padding(.vertical, WerkoutTheme.xs)
}
}
// MARK: - Actions
private func clearAllFilters() {
selectedMuscles.removeAll()
selectedEquipment.removeAll()
filteredRegisterdUser = nil
currentSort = nil
sortAscending = true
}
func maybeRefreshData() {
if userStore.token != nil{
if userStore.token != nil {
if userStore.plannedWorkouts.isEmpty {
userStore.fetchPlannedWorkouts()
}
if needsUpdating {
self.isUpdating = true
dataStore.fetchAllData(completion: {
DispatchQueue.main.async {
guard let allWorkouts = dataStore.allWorkouts else {
self.isUpdating = false
return
}
self.workouts = allWorkouts.sorted(by: {
$0.createdAt ?? Date() < $1.createdAt ?? Date()
})
self.isUpdating = false
self.uniqueWorkoutUsers = dataStore.workoutsUniqueUsers
self.needsUpdating = false
}
})
@@ -148,7 +342,7 @@ struct AllWorkoutsView: View {
showLoginView = true
}
}
func authorizeHealthKit() {
let quantityTypes = [
HKObjectType.quantityType(forIdentifier: .heartRate),
@@ -162,7 +356,7 @@ struct AllWorkoutsView: View {
HKQuantityType.workoutType()
]
)
healthStore.requestAuthorization(toShare: nil, read: healthKitTypes) { (success, error) in
if success == false {
runtimeReporter.recordWarning(
@@ -176,6 +370,6 @@ struct AllWorkoutsView: View {
struct AllWorkoutsView_Previews: PreviewProvider {
static var previews: some View {
AllWorkoutsView(workouts: PreviewData.allWorkouts())
AllWorkoutsView()
}
}