fix: codebase audit fixes — safety, accessibility, and production hygiene

Address 16 issues from external audit:
- Move StoreKit transaction listener ownership to StoreManager singleton with proper deinit
- Remove noisy VoiceOver announcements, add missing accessibility on StatPill and BootstrapLoadingView
- Replace String @retroactive Identifiable with IdentifiableShareCode wrapper
- Add crash guard in AchievementEngine getContributingVisitIds + cache stadium lookups
- Pre-compute GamesHistoryViewModel filtered properties to avoid redundant SwiftUI recomputation
- Remove force-unwraps in ProgressMapView with safe guard-let fallback
- Add diff-based update gating in ItineraryTableViewWrapper to prevent unnecessary reloads
- Replace deprecated UIScreen.main with UIWindowScene lookup
- Add deinit task cancellation in ScheduleViewModel and SuggestedTripsGenerator
- Wrap ~234 unguarded print() calls across 27 files in #if DEBUG

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-22 00:07:53 -06:00
parent 826eadbc0f
commit 91c5eac22d
32 changed files with 434 additions and 67 deletions

View File

@@ -53,6 +53,10 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
class Coordinator {
var headerHostingController: UIHostingController<HeaderContent>?
var lastStopCount: Int = 0
var lastGameIDsHash: Int = 0
var lastItemCount: Int = 0
var lastOverrideCount: Int = 0
}
func makeUIViewController(context: Context) -> ItineraryTableViewController {
@@ -73,7 +77,9 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
// Pre-size the header view
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
let targetWidth = UIScreen.main.bounds.width
let targetWidth = UIApplication.shared.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.screen.bounds.width }
.first ?? 390
let targetSize = CGSize(width: targetWidth, height: UIView.layoutFittingCompressedSize.height)
let size = hostingController.view.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
hostingController.view.frame = CGRect(origin: .zero, size: CGSize(width: targetWidth, height: max(size.height, 450)))
@@ -109,6 +115,25 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
// This avoids recreating the view hierarchy and prevents infinite loops
context.coordinator.headerHostingController?.rootView = headerContent
// Diff inputs before rebuilding to avoid unnecessary reloads
let currentStopCount = trip.stops.count
let currentGameIDsHash = games.map(\.game.id).hashValue
let currentItemCount = itineraryItems.count
let currentOverrideCount = travelOverrides.count
let coord = context.coordinator
guard currentStopCount != coord.lastStopCount ||
currentGameIDsHash != coord.lastGameIDsHash ||
currentItemCount != coord.lastItemCount ||
currentOverrideCount != coord.lastOverrideCount else {
return
}
coord.lastStopCount = currentStopCount
coord.lastGameIDsHash = currentGameIDsHash
coord.lastItemCount = currentItemCount
coord.lastOverrideCount = currentOverrideCount
let (days, validRanges, allItemsForConstraints, travelSegmentIndices) = buildItineraryData()
controller.reloadData(
days: days,