Overview
Every service in the iOS app follows the same pattern:
- A protocol defines the interface
- A Real implementation talks to the backend API
- A Fake implementation returns canned data for previews and tests
- An EnvironmentKey extension makes the service available via
@Environment
// The pattern — repeated for every service
protocol ScheduleService {
func fetchSchedule(for date: Date) async throws -> ScheduleResponse
}
class RealScheduleService: ScheduleService { /* hits API */ }
class FakeScheduleService: ScheduleService { /* returns mock data */ }
// Environment injection
@Environment(\.scheduleService) private var scheduleService
AuthService
Protocol: AuthService
Real implementation: CognitoAuthService
Handles all Cognito-based authentication flows.
| Method | Purpose |
|---|
signUp(email:password:) | Create a new Cognito user |
signIn(email:password:) | Authenticate and receive tokens |
verifyEmail(code:) | Confirm email with verification code |
resetPassword(email:) | Initiate forgot-password flow |
confirmResetPassword(code:newPassword:) | Complete password reset |
signOut() | Clear tokens and session |
currentUser | The currently authenticated user (if any) |
Admin detection
Admin status is determined by checking the JWT token’s cognito:groups claim. If the user belongs to the admin group, AppUser.isAdmin is true.
Admin detection is read-only on the client. The backend enforces admin permissions independently via its own JWT validation.
ScheduleService
Protocol: ScheduleService
Real implementation: RealScheduleService
Fetches and caches match schedules.
| Feature | Detail |
|---|
| Sliding-window cache | Keeps N days of schedule data in memory to avoid redundant fetches |
| Date-based filtering | Filters cached data by selected date before returning to the view |
| Debouncing | Rapid date changes debounce API calls to avoid flooding the backend |
| Today refresh | Always re-fetches today’s schedule to ensure live matches are current |
// Typical usage from a ViewModel
let schedule = try await scheduleService.fetchSchedule(for: selectedDate)
// Returns [LeagueGroup] — pre-grouped and pre-sorted by the backend
LiveActivityService
Protocol: LiveActivityService
Real implementation: RealLiveActivityService
Manages the full Live Activity lifecycle.
| Responsibility | Detail |
|---|
| Start activity | Calls Activity.request() with push token support |
| Observe push tokens | Listens to pushTokenUpdates and sends tokens to the backend |
| Follow/unfollow | Tracks followed matches, syncs with backend |
| End activity | Ends activities on unfollow, match completion, or stale detection |
| Stale detection | Ends activities with no push update for 30 minutes |
| State persistence | Stores followedMatchIds in UserDefaults |
The LiveActivityService is the most stateful service in the app. Be careful with the interaction between followedMatchIds (persisted), activeActivityId (in-memory), and pendingUnfollows (in-memory).
NotificationService
Protocol: NotificationService
Real implementation: RealNotificationService
Handles push notification permissions and device registration.
| Method | Purpose |
|---|
requestPermission() | Prompts the user for notification permission |
registerDevice(token:) | Sends the APNs device token to the backend |
unregisterDevice() | Tells the backend to stop sending pushes to this device |
AppConfigService
Protocol: AppConfigService
Real implementation: RealAppConfigService
Checks app configuration on launch.
| Feature | Detail |
|---|
| Version check | Compares local app version against minimum required version from backend |
| Force update | If the app is below minimum version, blocks usage and shows update prompt |
| Maintenance mode | If backend reports maintenance, shows a maintenance screen |
The version check runs on every app launch. The backend controls the minimum version, so you can force-update users without an App Store review cycle.
AdminService
Protocol: AdminService
Real implementation: RealAdminService
Debug-only service for controlling the fake API and tape playback.
| Feature | Detail |
|---|
| Fake API controls | Toggle fake API mode, switch between canned responses |
| Tape playback | Replay recorded API responses for testing Live Activity flows |
This service is only available in debug builds. It connects to fake-api.sightscreen.app for testing.
ImageCacheService
Protocol: ImageCacheService
Real implementation: RealImageCacheService
Two-tier image cache for team logos and other assets.
| Tier | Detail |
|---|
| Memory cache | NSCache-based, evicted under memory pressure |
| Disk cache | File-based, stored in the app group shared container |
The disk cache uses the app group container so that both the main app and the widget target can access cached images. This avoids duplicate downloads for team logos shown in Live Activities.
DataSource protocol
A generic pattern for cacheable, pollable data sources.
protocol DataSource<T> {
associatedtype T
var data: T? { get }
func fetch() async throws -> T
func startPolling(interval: TimeInterval)
func stopPolling()
}
Two concrete implementations:
| DataSource | Purpose |
|---|
LiveMatchesDataSource | Polls the live matches endpoint at a configurable interval |
ScheduleDataSource | Fetches schedule data with caching and date-based invalidation |
The DataSource pattern provides a consistent interface for any data that needs to be fetched, cached, and optionally polled.
DI wiring
All services are wired at the app entry point and injected into the SwiftUI environment:
@main
struct SightscreenApp: App {
var body: some Scene {
WindowGroup {
RootView()
.environment(\.authService, RealCognitoAuthService())
.environment(\.scheduleService, RealScheduleService())
.environment(\.liveActivityService, RealLiveActivityService())
.environment(\.notificationService, RealNotificationService())
.environment(\.appConfigService, RealAppConfigService())
.environment(\.imageCacheService, RealImageCacheService())
}
}
}
For SwiftUI previews, swap in fakes:
#Preview {
MatchListView()
.environment(\.scheduleService, FakeScheduleService())
}