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