Skip to main content

Overview

Every service in the iOS app follows the same pattern:
  1. A protocol defines the interface
  2. A Real implementation talks to the backend API
  3. A Fake implementation returns canned data for previews and tests
  4. 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.
MethodPurpose
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
currentUserThe 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.
FeatureDetail
Sliding-window cacheKeeps N days of schedule data in memory to avoid redundant fetches
Date-based filteringFilters cached data by selected date before returning to the view
DebouncingRapid date changes debounce API calls to avoid flooding the backend
Today refreshAlways 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.
ResponsibilityDetail
Start activityCalls Activity.request() with push token support
Observe push tokensListens to pushTokenUpdates and sends tokens to the backend
Follow/unfollowTracks followed matches, syncs with backend
End activityEnds activities on unfollow, match completion, or stale detection
Stale detectionEnds activities with no push update for 30 minutes
State persistenceStores 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.
MethodPurpose
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.
FeatureDetail
Version checkCompares local app version against minimum required version from backend
Force updateIf the app is below minimum version, blocks usage and shows update prompt
Maintenance modeIf 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.
FeatureDetail
Fake API controlsToggle fake API mode, switch between canned responses
Tape playbackReplay 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.
TierDetail
Memory cacheNSCache-based, evicted under memory pressure
Disk cacheFile-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:
DataSourcePurpose
LiveMatchesDataSourcePolls the live matches endpoint at a configurable interval
ScheduleDataSourceFetches 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())
}