Gym Tracker#
iOS app for tracking Virginia Tech gym occupancy and displaying campus events. Supports iOS, watchOS, and WidgetKit extensions.
Architecture#
Core Services#
GymService (GymService.swift)
- Singleton
@MainActorclass managing gym occupancy state - Publishes
@Publishedproperties:mcComasOccupancy,warMemorialOccupancy,boulderingWallOccupancy - Automatic refresh: 30-second interval when app is active (via
UIApplication.didBecomeActiveNotification) - Retry logic: 60-second delay if all fetches fail
- Uses
GymOccupancyFetcherfor data fetching
GymOccupancyFetcher (GymOccupancyFetcher.swift)
- Static enum performing HTTP requests
- Concurrent fetching:
async letfor parallel facility requests - POST request to
https://connect.recsports.vt.edu/FacilityOccupancy/GetFacilityData - Request body:
facilityId={uuid}&occupancyDisplayType={uuid}(URL-encoded) - Response: HTML containing
data-occupancyanddata-remainingattributes - Parsing: Delegates to
OccupancyHTMLParserfor regex extraction
OccupancyHTMLParser (OccupancyHTMLParser.swift)
- Regex-based HTML parsing (no external dependencies)
- Pattern:
data-occupancy="([0-9]+)"anddata-remaining="([0-9]+)" - Extracts occupancy and remaining capacity integers from HTML response
EventsViewModel (EventsViewModel.swift)
- Fetches RSS feed from
https://gobblerconnect.vt.edu/organization/www_recsports_vt_edu/events.rss - XML parsing via
XMLParserdelegate pattern (RSSParserclass) - Caching: JSON file in app caches directory (
cachedEvents.json) - Filters: Only caches events where
endDate > Date() - Network monitoring: Observes
NetworkMonitor.isConnectedvia Combine
Data Flow#
Gym Occupancy:
ContentView.onAppear→fetchGymOccupancyData()GymService.fetchAllGymOccupancy()→GymOccupancyFetcher.fetchAll()- Concurrent POST requests for three facilities (McComas, War Memorial, Bouldering Wall)
- HTML response parsed via regex for
data-occupancy/data-remaining - Results stored in
GymService@Publishedproperties - UI updates via SwiftUI
@ObservedObjectbinding
Events:
ContentView.onAppear→eventsViewModel.fetchEvents()- RSS feed fetched via
URLSession.dataTask - XML parsed via
RSSParser(NSXMLParser delegate) - Events filtered by
endDate > Date() - Cached to JSON file, loaded on init if available
Widgets:
UnifiedGymTrackerProvider.getTimeline()called by WidgetKitGymOccupancyFetcher.fetchForWidget()fetches occupancy (no remaining capacity)- Results stored in App Group UserDefaults (
group.VTGymApp.D8VXFBV8SJ) - Fallback to cached values if fetch fails
- Timeline policy:
.after(15 minutes)
Facility IDs & Capacities#
mcComasFacilityId: "da73849e-434d-415f-975a-4f9e799b9c39"
warMemorialFacilityId: "55069633-b56e-43b7-a68a-64d79364988d"
boulderingWallFacilityId: "da838218-ae53-4c6f-b744-2213299033fc"
mcComasMaxCapacity: 600
warMemorialMaxCapacity: 1200
boulderingWallMaxCapacity: 8
Barcode System#
Storage:
@AppStorage("gymBarcode")stores Codabar string (e.g., "A12345B")- Optional
@AppStorage("faceIDEnabled")for biometric protection
Scanning:
BarcodeScannerViewusesCodeScannerView(third-party library)- Scans Codabar format (
codeTypes: [.codabar]) - Validation: Ensures start/end characters are A/B/C/D
- Auto-prefix/suffix if missing: defaults to "A" prefix, "B" suffix
Generation:
BarcodeGeneratorusesCDCodabarView(third-party library)- Generates
UIImagefrom Codabar string - Displayed in
BarcodeDisplayOverlayViewwith brightness boost viaBrightnessManager
Authentication:
LocalAuthenticationframework for Face ID/Touch IDLAContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics)- Triggered before displaying stored barcode if
faceIDEnabled == true
App Group & Shared Storage#
- App Group ID:
group.VTGymApp.D8VXFBV8SJ - Shared UserDefaults keys:
mcComasOccupancy(Int)warMemorialOccupancy(Int)boulderingWallOccupancy(Int)lastFetchDate(Date)
- Used by: Main app, Widget extension, Watch app
UI Components#
ContentView:
NavigationStackwith groupedList- Sections: War Memorial, McComas, Bouldering Wall, Events
OccupancyCarddisplays occupancy with segmented progress bar- Color thresholds: Green (0-50%), Orange (50-75%), Maroon (75-100%)
OccupancyCard:
- Segmented progress visualization
- Displays occupancy count and remaining capacity
- Network status indicator
EventCard:
- Displays event title, location, time, hosting organization
- Date range: Today through 14 days ahead
Widget System#
UnifiedGymTrackerProvider:
- Single timeline provider for all widget sizes
getTimeline()fetches occupancy data- Stores in App Group UserDefaults
- Timeline refresh: 15-minute intervals
- Widget types: Home screen widgets, Lock screen widgets
Watch App#
WatchFacilitiesViewdisplays gym occupancyWatchGymCardViewshows individual facility statusWatchSegmentedProgressBarvisual indicator- Shares data via App Group UserDefaults
Dependencies#
- CodeScanner: Barcode scanning (
CodeScannerView) - CDCodabarView: Codabar barcode generation
- SwiftUI: UI framework
- Combine: Reactive data flow
- WidgetKit: Widget system
- LocalAuthentication: Biometric authentication
- AVFoundation: Camera access for barcode scanning
Build Requirements#
- iOS 17.0+
- watchOS 10.0+
- Xcode 16.0+
- Swift 5.9+
Project Structure#
Gym Tracker (RC)/
├── Services/
│ ├── GymService.swift # Main occupancy service
│ ├── GymOccupancyFetcher.swift # HTTP fetching
│ ├── OccupancyHTMLParser.swift # Regex HTML parsing
│ ├── Constants.swift # Facility IDs, capacities, URLs
│ └── UnifiedGymTrackerProvider.swift # Widget timeline provider
├── Events/
│ ├── EventsViewModel.swift # RSS fetching & caching
│ └── Event.swift # Event model
├── BarCode Scanner/
│ ├── BarcodeScannerView.swift # Camera scanning UI
│ ├── BarcodeGenerator.swift # Codabar image generation
│ ├── BarcodeDisplayView.swift # Display overlay
│ ├── ManualIDInputView.swift # Manual entry UI
│ └── BrightnessManager.swift # Screen brightness control
├── ContentView.swift # Main app view
└── OccupancyCard.swift # Occupancy display component
GymTrackerWidget/ # Widget extension
GymTrackerWatch Watch App/ # Watch app
GymTrackerComplications/ # Watch complications
License#
This project is licensed under the MIT License. See the LICENSE file for full text.
Virginia Tech is not associated with this project