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,88 @@ enum SortType: String, CaseIterable {
}
struct AllWorkoutsListView: View {
@State var searchNameString: String = ""
@State var selectedMuscles: Set<String> = []
@State var selectedEquipment: Set<String> = []
@Binding var uniqueWorkoutUsers: [RegisteredUser]?
@State private var filteredRegisterdUser: RegisteredUser?
let workouts: [Workout]
@Binding var searchText: String
@Binding var selectedMuscles: Set<String>
@Binding var selectedEquipment: Set<String>
@Binding var filteredRegisterdUser: RegisteredUser?
@Binding var currentSort: SortType?
@Binding var sortAscending: Bool
let selectedWorkout: ((Workout) -> Void)
@State var filteredWorkouts = [Workout]()
var refresh: (() -> Void)
@State var currentSort: SortType?
@State private var filteredWorkouts = [Workout]()
var body: some View {
VStack {
VStack {
if let filteredRegisterdUser = filteredRegisterdUser {
Text((filteredRegisterdUser.firstName ?? "NA") + "'s Workouts")
}
FilterAllView(selectedMuscles: $selectedMuscles,
selectedEquipment: $selectedEquipment,
searchNameString: $searchNameString,
uniqueWorkoutUsers: $uniqueWorkoutUsers,
filteredRegisterdUser: $filteredRegisterdUser,
filteredWorkouts: $filteredWorkouts,
workouts: .constant(workouts),
currentSort: $currentSort)
}
ScrollView {
LazyVStack(spacing: 10) {
ForEach(filteredWorkouts, id:\.id) { workout in
Button(action: {
selectedWorkout(workout)
}, label: {
WorkoutOverviewView(workout: workout)
.padding([.leading, .trailing], 4)
.contentShape(Rectangle())
})
.buttonStyle(.plain)
.accessibilityLabel("Open \(workout.name)")
.accessibilityHint("Shows workout details")
}
ScrollView {
LazyVStack(spacing: WerkoutTheme.sm) {
ForEach(filteredWorkouts, id: \.id) { workout in
Button(action: {
selectedWorkout(workout)
}, label: {
WorkoutOverviewView(workout: workout)
.padding(.horizontal, WerkoutTheme.sm)
.contentShape(Rectangle())
})
.buttonStyle(.plain)
.accessibilityLabel("Open \(workout.name)")
.accessibilityHint("Shows workout details")
}
}
.background(Color(uiColor: .systemBackground))
.refreshable {
refresh()
}
.padding(.top, WerkoutTheme.sm)
}
.onChange(of: searchNameString) { newValue in
filterWorkouts()
.scrollEdgeEffectStyle(.soft, for: .top)
.background(WerkoutTheme.background)
.refreshable {
refresh()
}
.onChange(of: selectedMuscles) { newValue in
filterWorkouts()
}
.onChange(of: selectedEquipment) { newValue in
filterWorkouts()
}
.onAppear{
filterWorkouts()
}
.onChange(of: workouts) { _ in
filterWorkouts()
.onAppear {
applyFilters()
}
.onChange(of: searchText) { applyFilters() }
.onChange(of: selectedMuscles) { applyFilters() }
.onChange(of: selectedEquipment) { applyFilters() }
.onChange(of: filteredRegisterdUser) { applyFilters() }
.onChange(of: currentSort) { applyFilters() }
.onChange(of: sortAscending) { applyFilters() }
.onChange(of: workouts) { applyFilters() }
}
func filterWorkouts() {
filteredWorkouts = workouts.filterWorkouts(
nameSearchString: searchNameString,
private func applyFilters() {
var results = workouts.filterWorkouts(
nameSearchString: searchText,
musclesSearchString: selectedMuscles,
equipmentSearchString: selectedEquipment,
filteredRegisterdUser: filteredRegisterdUser
)
if let sort = currentSort {
switch sort {
case .name:
results.sort { $0.name < $1.name }
case .createdDate:
results.sort { ($0.createdAt ?? Date()) < ($1.createdAt ?? Date()) }
}
if !sortAscending {
results.reverse()
}
}
filteredWorkouts = results
}
}
struct AllWorkoutsListView_Previews: PreviewProvider {
static var previews: some View {
AllWorkoutsListView(uniqueWorkoutUsers: .constant([]),
workouts: PreviewData.allWorkouts(),
selectedWorkout: { workout in },
refresh: { })
AllWorkoutsListView(workouts: PreviewData.allWorkouts(),
searchText: .constant(""),
selectedMuscles: .constant([]),
selectedEquipment: .constant([]),
filteredRegisterdUser: .constant(nil),
currentSort: .constant(nil),
sortAscending: .constant(true),
selectedWorkout: { _ in },
refresh: {})
}
}

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()
}
}