my swift app for VT gyms gymtracker.jackhannon.net

Data Flow documentation

+267
+267
API_DATA_FLOW.md
···
··· 1 + # API Data Flow Explainer 2 + 3 + This document explains exactly how the VT Gym Tracker app retrieves and processes gym occupancy data from the Virginia Tech RecSports API. 4 + 5 + ## API Endpoint 6 + 7 + **URL:** `https://connect.recsports.vt.edu/FacilityOccupancy/GetFacilityData` 8 + 9 + **Method:** `POST` 10 + 11 + **Content-Type:** `application/x-www-form-urlencoded` 12 + 13 + **Note:** This is not a REST API endpoint. It requires POST requests with form-encoded data. 14 + 15 + ## Request Format 16 + 17 + Each request requires two parameters in the request body: 18 + 19 + ``` 20 + facilityId={UUID}&occupancyDisplayType={UUID} 21 + ``` 22 + 23 + ### Facility IDs 24 + 25 + The app tracks three facilities: 26 + 27 + - **McComas Hall:** `da73849e-434d-415f-975a-4f9e799b9c39` 28 + - **War Memorial Hall:** `55069633-b56e-43b7-a68a-64d79364988d` 29 + - **Bouldering Wall:** `da838218-ae53-4c6f-b744-2213299033fc` 30 + 31 + ### Occupancy Display Type 32 + 33 + A constant UUID required by the API: `00000000-0000-0000-0000-000000004490` 34 + 35 + ### Example Request 36 + 37 + ```http 38 + POST /FacilityOccupancy/GetFacilityData HTTP/1.1 39 + Host: connect.recsports.vt.edu 40 + Content-Type: application/x-www-form-urlencoded 41 + 42 + facilityId=da73849e-434d-415f-975a-4f9e799b9c39&occupancyDisplayType=00000000-0000-0000-0000-000000004490 43 + ``` 44 + 45 + ## Response Format 46 + 47 + The API returns **HTML**, not JSON. The HTML contains occupancy data in `data-*` attributes on HTML elements. 48 + 49 + ### Example Response Structure 50 + 51 + ```html 52 + <canvas class="occupancy-chart" 53 + data-occupancy="275" 54 + data-remaining="325"> 55 + <!-- Chart rendering code --> 56 + </canvas> 57 + ``` 58 + 59 + The app extracts two values: 60 + - `data-occupancy`: Current number of people in the facility 61 + - `data-remaining`: Remaining capacity 62 + 63 + ## Data Flow 64 + 65 + ### 1. Request Initiation 66 + 67 + **Entry Point:** `GymService.fetchAllGymOccupancy()` 68 + 69 + The app fetches data for all three facilities concurrently using Swift's `async let`: 70 + 71 + ```swift 72 + async let mc = fetchOne(facilityId: Constants.mcComasFacilityId) 73 + async let wm = fetchOne(facilityId: Constants.warMemorialFacilityId) 74 + async let bw = fetchOne(facilityId: Constants.boulderingWallFacilityId) 75 + let (m, w, b) = await (mc, wm, bw) 76 + ``` 77 + 78 + This parallel fetching improves performance by requesting all facilities simultaneously rather than sequentially. 79 + 80 + ### 2. HTTP Request (`GymOccupancyFetcher.fetchOne`) 81 + 82 + For each facility: 83 + 84 + 1. **Create URLRequest:** 85 + ```swift 86 + var req = URLRequest(url: Constants.facilityDataAPIURL) 87 + req.httpMethod = "POST" 88 + req.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") 89 + ``` 90 + 91 + 2. **Build Request Body:** 92 + ```swift 93 + req.httpBody = "facilityId=\(facilityId)&occupancyDisplayType=\(Constants.occupancyDisplayType)".data(using: .utf8) 94 + ``` 95 + 96 + 3. **Execute Request:** 97 + ```swift 98 + let (data, response) = try await urlSession.data(for: req) 99 + ``` 100 + 101 + 4. **Validate Response:** 102 + - Check HTTP status code is 200-299 103 + - Verify data exists 104 + - Convert data to UTF-8 string 105 + 106 + ### 3. URLSession Configuration 107 + 108 + The app uses a custom `URLSession` configuration: 109 + 110 + ```swift 111 + let c = URLSessionConfiguration.default 112 + c.timeoutIntervalForRequest = 30 // 30 second timeout 113 + c.requestCachePolicy = .reloadIgnoringLocalCacheData // Always fetch fresh data 114 + ``` 115 + 116 + **Why no caching?** Occupancy data changes frequently. Caching would show stale counts. 117 + 118 + ### 4. HTML Parsing (`OccupancyHTMLParser.parse`) 119 + 120 + The HTML response is parsed using regex to extract the `data-occupancy` and `data-remaining` attributes: 121 + 122 + **Pattern:** `data-occupancy="([0-9]+)"` and `data-remaining="([0-9]+)"` 123 + 124 + **Process:** 125 + 1. Search HTML string for attribute pattern 126 + 2. Extract captured group (the numeric value) 127 + 3. Convert to `Int` 128 + 4. Return tuple: `(occupancy: Int, remaining: Int)?` 129 + 130 + **Why regex instead of HTML parser?** 131 + - No external dependencies required 132 + - Simple attribute extraction doesn't need full HTML parsing 133 + - Lightweight and fast 134 + 135 + ### 5. Data Processing (`GymService.storeAndNotify`) 136 + 137 + After parsing: 138 + 139 + 1. **Update Published Properties:** 140 + ```swift 141 + self.mcComasOccupancy = mcComasData?.occupancy 142 + self.warMemorialOccupancy = warMemorialData?.occupancy 143 + self.boulderingWallOccupancy = boulderingWallData?.occupancy 144 + ``` 145 + These `@Published` properties trigger SwiftUI view updates. 146 + 147 + 2. **Store in App Group UserDefaults:** 148 + ```swift 149 + let sharedDefaults = UserDefaults(suiteName: Constants.appGroupID) 150 + sharedDefaults.set(mc, forKey: "mcComasOccupancy") 151 + ``` 152 + This allows widgets and the watch app to access the data. 153 + 154 + 3. **Notify Widgets:** 155 + ```swift 156 + WidgetCenter.shared.reloadAllTimelines() 157 + ``` 158 + Widgets are immediately notified to refresh with new data. 159 + 160 + ## Refresh Schedule 161 + 162 + ### Foreground (App Active) 163 + 164 + - **Interval:** 30 seconds 165 + - **Trigger:** Timer fires when app is active (`UIApplication.didBecomeActiveNotification`) 166 + - **Stops:** When app backgrounds (`UIApplication.willResignActiveNotification`) 167 + 168 + **Why 30 seconds?** Balances data freshness with battery and network usage. 169 + 170 + ### Background/Widgets 171 + 172 + - **Widget Timeline:** Requests refresh every 15 minutes (WidgetKit managed) 173 + - **Widgets read from:** App Group UserDefaults (shared storage) 174 + 175 + ## Error Handling 176 + 177 + ### Network Failures 178 + 179 + 1. **All Facilities Fail:** 180 + - `isOnline` set to `false` 181 + - Retry scheduled after 60 seconds 182 + - Prevents immediate retry loop 183 + 184 + 2. **Partial Success:** 185 + - If any facility succeeds, `isOnline` remains `true` 186 + - Only successful facilities update their values 187 + - Failed facilities retain previous values 188 + 189 + ### Parsing Failures 190 + 191 + - If HTML parsing fails, `fetchOne` returns `nil` 192 + - The facility's occupancy remains unchanged 193 + - No error is thrown; the app continues with available data 194 + 195 + ## Data Storage 196 + 197 + ### In-App (`GymService`) 198 + 199 + - `@Published` properties for SwiftUI binding 200 + - Updated on main thread (`@MainActor`) 201 + 202 + ### Shared Storage (App Group) 203 + 204 + **App Group ID:** `group.VTGymApp.D8VXFBV8SJ` 205 + 206 + **Keys:** 207 + - `mcComasOccupancy` (Int) 208 + - `warMemorialOccupancy` (Int) 209 + - `boulderingWallOccupancy` (Int) 210 + - `lastFetchDate` (Date) 211 + 212 + **Used by:** 213 + - Main iOS app 214 + - Widget extensions 215 + - Watch app 216 + 217 + ## Testing/Debugging Override 218 + 219 + The app supports custom occupancy values for testing: 220 + 221 + ```swift 222 + @Published var useCustomOccupancy: Bool = false 223 + @Published var customMcComasOccupancy: Int? = 275 224 + @Published var customWarMemorialOccupancy: Int? = 1025 225 + @Published var customBoulderingWallOccupancy: Int? = 6 226 + ``` 227 + 228 + When enabled, these values override API data. Useful for: 229 + - Testing UI without network 230 + - Debugging display logic 231 + - Demonstrating app functionality 232 + 233 + ## Complete Flow Diagram 234 + 235 + ``` 236 + ContentView.onAppear 237 + 238 + GymService.fetchAllGymOccupancy() 239 + 240 + GymOccupancyFetcher.fetchAll() 241 + ├─→ fetchOne(McComas) ──┐ 242 + ├─→ fetchOne(War Memorial) ──┤ Concurrent HTTP POST requests 243 + └─→ fetchOne(Bouldering) ────┘ 244 + 245 + POST https://connect.recsports.vt.edu/FacilityOccupancy/GetFacilityData 246 + Body: facilityId={UUID}&occupancyDisplayType={UUID} 247 + 248 + HTML Response with data-occupancy and data-remaining attributes 249 + 250 + OccupancyHTMLParser.parse(html) 251 + 252 + Regex extraction: data-occupancy="([0-9]+)" and data-remaining="([0-9]+)" 253 + 254 + Return (occupancy: Int, remaining: Int)? 255 + 256 + GymService.storeAndNotify() 257 + ├─→ Update @Published properties (SwiftUI updates) 258 + ├─→ Store in App Group UserDefaults (widgets/watch access) 259 + └─→ WidgetCenter.reloadAllTimelines() (notify widgets) 260 + ``` 261 + 262 + ## Key Files 263 + 264 + - **`GymOccupancyFetcher.swift`:** HTTP requests and concurrent fetching 265 + - **`OccupancyHTMLParser.swift`:** HTML parsing via regex 266 + - **`GymService.swift`:** Data management and refresh scheduling 267 + - **`Constants.swift`:** API URLs, facility IDs, and configuration