Enrollment Polling with Timer and @MainActor
For our Music Theory Learning app, we implemented student enrollment and faced an interesting architectural decision. Rather than maintaining a Supabase Realtime WebSocket connection, a lightweight Timer polls every 10 minutes for pending enrollment requests. The badge counter updates automatically, and the approach is easy to reason about and debug—a pragmatic trade-off that works well for low-frequency events. Running the update logic on @MainActor ensures that UI state changes remain thread-safe and predictable. This design also reduces infrastructure complexity while keeping resource usage low.
func startEnrollmentPolling() {
stopEnrollmentPolling()
enrollmentTimer = Timer.scheduledTimer(withTimeInterval: 60 * 10, repeats: true) { [weak self] _ in
Task { @MainActor [weak self] in
await self?.fetchPendingEnrollments()
}
}
}
func fetchPendingEnrollments() async {
do {
let requests = try await enrollmentService.fetchPendingEnrollments()
pendingEnrollments = requests.map { mapToUI($0) }
enrollmentCount = pendingEnrollments.count
hasNewEnrollments = enrollmentCount > 0 // drives the badge
} catch {
loadPendingEnrollmentsOffline() // graceful fallback to cache
}
}
