student-timekeeper-ios

DreamLauncherLite iOS - Technical Specification

Project Setup

Xcode Project Configuration

Project Name: DreamLauncherLite
Bundle Identifier: com.dreamlauncher.lite.ios
Minimum iOS Version: 16.0
Language: Swift 5.9+
UI Framework: SwiftUI
Architecture: MVVM + Services

Targets

  1. DreamLauncherLite (Main App)
    • Bundle ID: com.dreamlauncher.lite.ios
    • Capabilities:
      • Sign in with Apple
      • Family Controls
      • Background Modes (Background fetch, Background processing)
      • Keychain Sharing
  2. DeviceActivityMonitorExtension
    • Bundle ID: com.dreamlauncher.lite.ios.monitor
    • Extension Type: DeviceActivityMonitor
    • Parent: DreamLauncherLite
  3. DeviceActivityReportExtension
    • Bundle ID: com.dreamlauncher.lite.ios.report
    • Extension Type: DeviceActivityReport
    • Parent: DreamLauncherLite

Dependencies

Swift Package Manager:

// Package.swift
dependencies: [
    // No external dependencies - keep it simple!
    // All functionality uses Apple frameworks
]

Why no dependencies?


Architecture

MVVM + Services Pattern

┌─────────────────────────────────────────────┐
│              Views (SwiftUI)                │
│   AuthView, DashboardView, SettingsView    │
└─────────────┬───────────────────────────────┘
              │
              │ ObservableObject
              ▼
┌─────────────────────────────────────────────┐
│           ViewModels (@MainActor)           │
│  AuthViewModel, DashboardViewModel          │
└─────────────┬───────────────────────────────┘
              │
              │ async/await
              ▼
┌─────────────────────────────────────────────┐
│                Services                     │
│  AuthService, ScreenTimeService,            │
│  SyncService, APIClient                     │
└─────────────┬───────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────┐
│             Storage Layer                   │
│  Keychain, CoreData, UserDefaults           │
└─────────────────────────────────────────────┘

Core Services

1. AuthService

Purpose: Handle Apple Sign-In and token management

// Services/AuthService.swift
import AuthenticationServices
import Security

@MainActor
class AuthService: ObservableObject {
    @Published var isAuthenticated = false
    @Published var currentUser: User?
    
    private let apiClient: APIClient
    private let keychain = KeychainService()
    
    init(apiClient: APIClient = .shared) {
        self.apiClient = apiClient
        checkAuthStatus()
    }
    
    // MARK: - Authentication
    
    func signInWithApple(
        authorization: ASAuthorization
    ) async throws {
        guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
              let identityToken = credential.identityToken,
              let tokenString = String(data: identityToken, encoding: .utf8),
              let authCode = credential.authorizationCode,
              let codeString = String(data: authCode, encoding: .utf8)
        else {
            throw AuthError.invalidCredentials
        }
        
        // Call API
        let response = try await apiClient.signInWithApple(
            identityToken: tokenString,
            authorizationCode: codeString,
            email: credential.email,
            fullName: credential.fullName
        )
        
        // Store tokens
        try keychain.save(response.accessToken, for: .accessToken)
        try keychain.save(response.refreshToken, for: .refreshToken)
        
        // Update state
        currentUser = response.user
        isAuthenticated = true
    }
    
    func signOut() {
        keychain.delete(for: .accessToken)
        keychain.delete(for: .refreshToken)
        currentUser = nil
        isAuthenticated = false
    }
    
    func refreshTokenIfNeeded() async throws {
        guard let refreshToken = keychain.get(for: .refreshToken) else {
            throw AuthError.notAuthenticated
        }
        
        let response = try await apiClient.refreshToken(refreshToken)
        try keychain.save(response.accessToken, for: .accessToken)
        try keychain.save(response.refreshToken, for: .refreshToken)
    }
    
    // MARK: - Private
    
    private func checkAuthStatus() {
        isAuthenticated = keychain.get(for: .accessToken) != nil
    }
}

// MARK: - Supporting Types

enum AuthError: LocalizedError {
    case invalidCredentials
    case notAuthenticated
    case networkError(Error)
    
    var errorDescription: String? {
        switch self {
        case .invalidCredentials:
            return "Invalid credentials provided"
        case .notAuthenticated:
            return "User is not authenticated"
        case .networkError(let error):
            return "Network error: \(error.localizedDescription)"
        }
    }
}

struct AuthResponse: Codable {
    let accessToken: String
    let refreshToken: String
    let user: User
}

struct User: Codable, Identifiable {
    let id: String
    let appleId: String?
    let email: String?
    let name: String?
}

2. ScreenTimeService

Purpose: Interact with Family Controls framework

// Services/ScreenTimeService.swift
import FamilyControls
import DeviceActivity

class ScreenTimeService {
    private let authCenter = AuthorizationCenter.shared
    
    // MARK: - Authorization
    
    func requestAuthorization() async throws {
        let status = authCenter.authorizationStatus
        
        switch status {
        case .notDetermined:
            try await authCenter.requestAuthorization(for: .individual)
        case .denied:
            throw ScreenTimeError.authorizationDenied
        case .approved:
            return
        @unknown default:
            throw ScreenTimeError.unknownAuthStatus
        }
    }
    
    var isAuthorized: Bool {
        authCenter.authorizationStatus == .approved
    }
    
    // MARK: - Data Collection
    
    func fetchTodayScreenTime() async throws -> ScreenTimeData {
        guard isAuthorized else {
            throw ScreenTimeError.notAuthorized
        }
        
        let today = Calendar.current.startOfDay(for: Date())
        let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)!
        
        return try await fetchScreenTime(
            from: today,
            to: tomorrow
        )
    }
    
    func fetchScreenTime(from: Date, to: Date) async throws -> ScreenTimeData {
        // This is a simplified version - actual implementation
        // requires DeviceActivityReport extension
        
        return ScreenTimeData(
            startDate: from,
            endDate: to,
            totalMinutes: 0,  // Will be populated by report extension
            deviceType: currentDeviceType()
        )
    }
    
    func fetchWeekScreenTime() async throws -> [ScreenTimeData] {
        let today = Calendar.current.startOfDay(for: Date())
        let weekAgo = Calendar.current.date(byAdding: .day, value: -7, to: today)!
        
        var days: [ScreenTimeData] = []
        
        for dayOffset in 0..<7 {
            let date = Calendar.current.date(byAdding: .day, value: -dayOffset, to: today)!
            let nextDay = Calendar.current.date(byAdding: .day, value: 1, to: date)!
            
            let data = try await fetchScreenTime(from: date, to: nextDay)
            days.append(data)
        }
        
        return days
    }
    
    // MARK: - Private
    
    private func currentDeviceType() -> String {
        #if os(iOS)
        switch UIDevice.current.userInterfaceIdiom {
        case .phone:
            return "IOS_PHONE"
        case .pad:
            return "IOS_TABLET"
        default:
            return "UNKNOWN"
        }
        #else
        return "UNKNOWN"
        #endif
    }
}

// MARK: - Supporting Types

enum ScreenTimeError: LocalizedError {
    case notAuthorized
    case authorizationDenied
    case unknownAuthStatus
    case dataUnavailable
    
    var errorDescription: String? {
        switch self {
        case .notAuthorized:
            return "Screen Time access not authorized"
        case .authorizationDenied:
            return "User denied Screen Time authorization"
        case .unknownAuthStatus:
            return "Unknown authorization status"
        case .dataUnavailable:
            return "Screen Time data is not available"
        }
    }
}

struct ScreenTimeData {
    let startDate: Date
    let endDate: Date
    let totalMinutes: Int
    let deviceType: String
    
    var durationInMinutes: Int { totalMinutes }
}

3. SyncService

Purpose: Sync screen time data to backend

// Services/SyncService.swift
import Foundation
import CoreData

actor SyncService {
    private let apiClient: APIClient
    private let storage: StorageService
    private var isSyncing = false
    
    init(
        apiClient: APIClient = .shared,
        storage: StorageService = .shared
    ) {
        self.apiClient = apiClient
        self.storage = storage
    }
    
    // MARK: - Public API
    
    func sync() async throws {
        guard !isSyncing else {
            print("Sync already in progress")
            return
        }
        
        isSyncing = true
        defer { isSyncing = false }
        
        print("Starting screen time sync...")
        
        // Get unsynced entries
        let unsynced = try await storage.getUnsyncedEntries()
        
        guard !unsynced.isEmpty else {
            print("No unsynced entries")
            return
        }
        
        print("Syncing \(unsynced.count) entries")
        
        // Convert to DTOs
        let dtos = unsynced.map { CreateScreenTimeDTO(from: $0) }
        
        // Upload to API
        try await apiClient.uploadScreenTime(dtos)
        
        // Mark as synced
        let ids = unsynced.map { $0.id }
        try await storage.markAsSynced(ids)
        
        print("Sync completed successfully")
    }
    
    func syncInBackground() async {
        do {
            try await sync()
        } catch {
            print("Background sync failed: \(error)")
            // Don't throw - background sync should be resilient
        }
    }
    
    // MARK: - Background Task
    
    func scheduleBackgroundSync() {
        BackgroundTaskManager.shared.scheduleSync()
    }
}

// MARK: - DTOs

struct CreateScreenTimeDTO: Codable {
    let startDate: Date
    let endDate: Date
    let durationInMinutes: Int
    let timeZone: String
    
    init(from entry: ScreenTimeEntry) {
        self.startDate = entry.startDate
        self.endDate = entry.endDate
        self.durationInMinutes = Int(entry.durationInMinutes)
        self.timeZone = TimeZone.current.identifier
    }
}

4. APIClient

Purpose: Network layer for all API communication

// Services/APIClient.swift
import Foundation

class APIClient {
    static let shared = APIClient()
    
    private let baseURL: URL
    private let session: URLSession
    private let keychain = KeychainService()
    
    private init() {
        self.baseURL = URL(string: Config.apiBaseURL)!
        
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 300
        self.session = URLSession(configuration: config)
    }
    
    // MARK: - Authentication
    
    func signInWithApple(
        identityToken: String,
        authorizationCode: String,
        email: String?,
        fullName: PersonNameComponents?
    ) async throws -> AuthResponse {
        let endpoint = baseURL.appendingPathComponent("/api/v1/apple-auth/sign-in")
        
        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body: [String: Any?] = [
            "identityToken": identityToken,
            "authorizationCode": authorizationCode,
            "user": [
                "email": email,
                "firstName": fullName?.givenName,
                "lastName": fullName?.familyName
            ]
        ]
        
        request.httpBody = try JSONSerialization.data(
            withJSONObject: body.compactMapValues { $0 }
        )
        
        let (data, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }
        
        guard httpResponse.statusCode == 200 else {
            throw APIError.httpError(httpResponse.statusCode)
        }
        
        return try JSONDecoder().decode(AuthResponse.self, from: data)
    }
    
    func refreshToken(_ refreshToken: String) async throws -> AuthResponse {
        let endpoint = baseURL.appendingPathComponent("/api/v1/refresh-token")
        
        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = ["refreshToken": refreshToken]
        request.httpBody = try JSONEncoder().encode(body)
        
        let (data, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw APIError.invalidResponse
        }
        
        return try JSONDecoder().decode(AuthResponse.self, from: data)
    }
    
    // MARK: - Screen Time
    
    func uploadScreenTime(_ entries: [CreateScreenTimeDTO]) async throws {
        for entry in entries {
            try await uploadSingleEntry(entry)
        }
    }
    
    private func uploadSingleEntry(_ entry: CreateScreenTimeDTO) async throws {
        let endpoint = baseURL.appendingPathComponent("/api/v1/user-screen-time")
        
        var request = try await authorizedRequest(for: endpoint, method: "POST")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        request.httpBody = try encoder.encode(entry)
        
        let (_, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw APIError.uploadFailed
        }
    }
    
    func fetchScreenTime(
        startDate: Date,
        endDate: Date
    ) async throws -> [ScreenTimeResponseData] {
        var components = URLComponents(
            url: baseURL.appendingPathComponent("/api/v1/user-screen-time"),
            resolvingAgainstBaseURL: true
        )!
        
        let formatter = ISO8601DateFormatter()
        components.queryItems = [
            URLQueryItem(name: "startDate", value: formatter.string(from: startDate)),
            URLQueryItem(name: "endDate", value: formatter.string(from: endDate)),
            URLQueryItem(name: "limit", value: "1000")
        ]
        
        let request = try await authorizedRequest(for: components.url!, method: "GET")
        let (data, _) = try await session.data(for: request)
        
        let response = try JSONDecoder().decode(
            ScreenTimeResponse.self,
            from: data
        )
        
        return response.data
    }
    
    func fetchCombinedScreenTime(
        startDate: Date,
        endDate: Date
    ) async throws -> CombinedScreenTimeResponse {
        var components = URLComponents(
            url: baseURL.appendingPathComponent("/api/v1/user-screen-time/combined"),
            resolvingAgainstBaseURL: true
        )!
        
        let formatter = ISO8601DateFormatter()
        components.queryItems = [
            URLQueryItem(name: "startDate", value: formatter.string(from: startDate)),
            URLQueryItem(name: "endDate", value: formatter.string(from: endDate))
        ]
        
        let request = try await authorizedRequest(for: components.url!, method: "GET")
        let (data, _) = try await session.data(for: request)
        
        return try JSONDecoder().decode(
            CombinedScreenTimeResponse.self,
            from: data
        )
    }
    
    // MARK: - Private Helpers
    
    private func authorizedRequest(
        for url: URL,
        method: String
    ) async throws -> URLRequest {
        guard let token = keychain.get(for: .accessToken) else {
            throw APIError.notAuthenticated
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = method
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        
        return request
    }
}

// MARK: - Response Types

struct ScreenTimeResponse: Codable {
    let success: Bool
    let data: [ScreenTimeResponseData]
    let pagination: Pagination
}

struct ScreenTimeResponseData: Codable {
    let id: String
    let userId: String
    let startDate: Date
    let endDate: Date
    let durationInMinutes: Int
    let deviceType: String?
    let createdAt: Date
}

struct CombinedScreenTimeResponse: Codable {
    let success: Bool
    let data: [ScreenTimeResponseData]
    let platformBreakdown: PlatformBreakdown
}

struct PlatformBreakdown: Codable {
    let ios: Int
    let macos: Int
    let total: Int
}

struct Pagination: Codable {
    let total: Int
    let limit: Int
    let offset: Int
    let hasMore: Bool
}

// MARK: - Errors

enum APIError: LocalizedError {
    case invalidResponse
    case httpError(Int)
    case notAuthenticated
    case uploadFailed
    case decodingError(Error)
    
    var errorDescription: String? {
        switch self {
        case .invalidResponse:
            return "Invalid server response"
        case .httpError(let code):
            return "HTTP error: \(code)"
        case .notAuthenticated:
            return "User not authenticated"
        case .uploadFailed:
            return "Failed to upload data"
        case .decodingError(let error):
            return "Decoding error: \(error.localizedDescription)"
        }
    }
}

Storage Layer

Core Data Stack

// Storage/CoreDataStack.swift
import CoreData

class CoreDataStack {
    static let shared = CoreDataStack()
    
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "DreamLauncher")
        
        container.loadPersistentStores { description, error in
            if let error = error {
                fatalError("Unable to load persistent stores: \(error)")
            }
        }
        
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        return container
    }()
    
    var viewContext: NSManagedObjectContext {
        persistentContainer.viewContext
    }
    
    func newBackgroundContext() -> NSManagedObjectContext {
        persistentContainer.newBackgroundContext()
    }
    
    func save() throws {
        let context = viewContext
        
        guard context.hasChanges else { return }
        
        try context.save()
    }
}

Storage Service

// Storage/StorageService.swift
import CoreData

actor StorageService {
    static let shared = StorageService()
    
    private let stack = CoreDataStack.shared
    
    // MARK: - Screen Time Entries
    
    func saveScreenTimeEntry(_ data: ScreenTimeData) async throws {
        let context = stack.newBackgroundContext()
        
        try await context.perform {
            let entry = ScreenTimeEntry(context: context)
            entry.id = UUID()
            entry.startDate = data.startDate
            entry.endDate = data.endDate
            entry.durationInMinutes = Int32(data.totalMinutes)
            entry.deviceType = data.deviceType
            entry.synced = false
            entry.createdAt = Date()
            
            try context.save()
        }
    }
    
    func getUnsyncedEntries() async throws -> [ScreenTimeEntry] {
        let context = stack.newBackgroundContext()
        
        return try await context.perform {
            let request: NSFetchRequest<ScreenTimeEntry> = ScreenTimeEntry.fetchRequest()
            request.predicate = NSPredicate(format: "synced == NO")
            request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)]
            request.fetchLimit = 100
            
            return try context.fetch(request)
        }
    }
    
    func markAsSynced(_ ids: [UUID]) async throws {
        let context = stack.newBackgroundContext()
        
        try await context.perform {
            let request: NSFetchRequest<ScreenTimeEntry> = ScreenTimeEntry.fetchRequest()
            request.predicate = NSPredicate(format: "id IN %@", ids)
            
            let entries = try context.fetch(request)
            
            for entry in entries {
                entry.synced = true
                entry.syncedAt = Date()
            }
            
            try context.save()
        }
    }
    
    func getTodayScreenTime() async throws -> Int {
        let context = stack.viewContext
        
        return try await context.perform {
            let today = Calendar.current.startOfDay(for: Date())
            let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)!
            
            let request: NSFetchRequest<ScreenTimeEntry> = ScreenTimeEntry.fetchRequest()
            request.predicate = NSPredicate(
                format: "startDate >= %@ AND startDate < %@",
                today as NSDate,
                tomorrow as NSDate
            )
            
            let entries = try context.fetch(request)
            return entries.reduce(0) { $0 + Int($1.durationInMinutes) }
        }
    }
}

Keychain Service

// Storage/KeychainService.swift
import Security
import Foundation

class KeychainService {
    enum KeychainKey: String {
        case accessToken = "com.dreamlauncher.accessToken"
        case refreshToken = "com.dreamlauncher.refreshToken"
    }
    
    func save(_ value: String, for key: KeychainKey) throws {
        let data = value.data(using: .utf8)!
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue,
            kSecValueData as String: data
        ]
        
        // Delete existing
        SecItemDelete(query as CFDictionary)
        
        // Add new
        let status = SecItemAdd(query as CFDictionary, nil)
        
        guard status == errSecSuccess else {
            throw KeychainError.saveFailed(status)
        }
    }
    
    func get(for key: KeychainKey) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue,
            kSecReturnData as String: true
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        guard status == errSecSuccess,
              let data = result as? Data,
              let string = String(data: data, encoding: .utf8) else {
            return nil
        }
        
        return string
    }
    
    func delete(for key: KeychainKey) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue
        ]
        
        SecItemDelete(query as CFDictionary)
    }
}

enum KeychainError: Error {
    case saveFailed(OSStatus)
    case deleteFailed(OSStatus)
}

View Models

DashboardViewModel

// ViewModels/DashboardViewModel.swift
import SwiftUI
import Combine

@MainActor
class DashboardViewModel: ObservableObject {
    @Published var todayMinutes: Int = 0
    @Published var weekData: [DayData] = []
    @Published var platformBreakdown: PlatformBreakdown?
    @Published var isLoading = false
    @Published var error: Error?
    
    private let screenTimeService: ScreenTimeService
    private let syncService: SyncService
    private let apiClient: APIClient
    private let storage: StorageService
    
    init(
        screenTimeService: ScreenTimeService = ScreenTimeService(),
        syncService: SyncService = .init(),
        apiClient: APIClient = .shared,
        storage: StorageService = .shared
    ) {
        self.screenTimeService = screenTimeService
        self.syncService = syncService
        self.apiClient = apiClient
        self.storage = storage
    }
    
    func loadData() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            // Load today's local data
            todayMinutes = try await storage.getTodayScreenTime()
            
            // Load week data from API (includes macOS)
            let today = Date()
            let weekAgo = Calendar.current.date(byAdding: .day, value: -7, to: today)!
            
            let combined = try await apiClient.fetchCombinedScreenTime(
                startDate: weekAgo,
                endDate: today
            )
            
            platformBreakdown = combined.platformBreakdown
            weekData = processWeekData(combined.data)
            
        } catch {
            self.error = error
            print("Error loading data: \(error)")
        }
    }
    
    func refresh() async {
        // Sync local data
        try? await syncService.sync()
        
        // Reload
        await loadData()
    }
    
    private func processWeekData(_ data: [ScreenTimeResponseData]) -> [DayData] {
        // Group by day and sum durations
        var dayMap: [Date: DayData] = [:]
        
        for entry in data {
            let day = Calendar.current.startOfDay(for: entry.startDate)
            
            if var existing = dayMap[day] {
                existing.totalMinutes += entry.durationInMinutes
                
                if entry.deviceType?.starts(with: "IOS") == true {
                    existing.iosMinutes += entry.durationInMinutes
                } else if entry.deviceType == "MACOS" {
                    existing.macosMinutes += entry.durationInMinutes
                }
                
                dayMap[day] = existing
            } else {
                let iosMinutes = entry.deviceType?.starts(with: "IOS") == true ? entry.durationInMinutes : 0
                let macosMinutes = entry.deviceType == "MACOS" ? entry.durationInMinutes : 0
                
                dayMap[day] = DayData(
                    date: day,
                    totalMinutes: entry.durationInMinutes,
                    iosMinutes: iosMinutes,
                    macosMinutes: macosMinutes
                )
            }
        }
        
        return dayMap.values.sorted { $0.date < $1.date }
    }
}

struct DayData: Identifiable {
    let date: Date
    var totalMinutes: Int
    var iosMinutes: Int
    var macosMinutes: Int
    
    var id: Date { date }
    
    var formattedHours: String {
        let hours = Double(totalMinutes) / 60.0
        return String(format: "%.1fh", hours)
    }
}

Views

AuthView

// Views/AuthView.swift
import SwiftUI
import AuthenticationServices

struct AuthView: View {
    @EnvironmentObject var authService: AuthService
    @State private var isLoading = false
    @State private var error: Error?
    
    var body: some View {
        VStack(spacing: 40) {
            Spacer()
            
            // Logo
            Image(systemName: "moon.fill")
                .resizable()
                .frame(width: 100, height: 100)
                .foregroundColor(.blue)
            
            // Title
            VStack(spacing: 8) {
                Text("DreamLauncher")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                
                Text("Cross-platform screen time tracking")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            
            Spacer()
            
            // Sign in button
            SignInWithAppleButton(.signIn) { request in
                request.requestedScopes = [.email, .fullName]
            } onCompletion: { result in
                Task {
                    await handleSignIn(result)
                }
            }
            .signInWithAppleButtonStyle(.black)
            .frame(height: 50)
            .padding(.horizontal, 40)
            
            if let error = error {
                Text(error.localizedDescription)
                    .font(.caption)
                    .foregroundColor(.red)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal)
            }
            
            Spacer()
        }
        .overlay {
            if isLoading {
                ProgressView()
            }
        }
    }
    
    private func handleSignIn(_ result: Result<ASAuthorization, Error>) async {
        isLoading = true
        defer { isLoading = false }
        
        switch result {
        case .success(let authorization):
            do {
                try await authService.signInWithApple(authorization: authorization)
            } catch {
                self.error = error
            }
        case .failure(let error):
            self.error = error
        }
    }
}

DashboardView

// Views/DashboardView.swift
import SwiftUI

struct DashboardView: View {
    @StateObject private var viewModel = DashboardViewModel()
    @EnvironmentObject var authService: AuthService
    
    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 24) {
                    // Today's total
                    TodayCard(minutes: viewModel.todayMinutes)
                    
                    // Week chart
                    WeekChartView(days: viewModel.weekData)
                    
                    // Platform breakdown
                    if let breakdown = viewModel.platformBreakdown {
                        PlatformBreakdownView(breakdown: breakdown)
                    }
                }
                .padding()
            }
            .navigationTitle("Screen Time")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink {
                        SettingsView()
                    } label: {
                        Image(systemName: "gear")
                    }
                }
            }
            .refreshable {
                await viewModel.refresh()
            }
            .task {
                await viewModel.loadData()
            }
        }
    }
}

// MARK: - Subviews

struct TodayCard: View {
    let minutes: Int
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Today's Screen Time")
                .font(.headline)
                .foregroundColor(.secondary)
            
            Text(formattedDuration)
                .font(.system(size: 48, weight: .bold))
            
            ProgressView(value: Double(minutes), total: 720) // 12 hours max
                .tint(.blue)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding()
        .background(Color(.systemBackground))
        .cornerRadius(12)
        .shadow(radius: 2)
    }
    
    private var formattedDuration: String {
        let hours = minutes / 60
        let mins = minutes % 60
        return "\(hours)h \(mins)m"
    }
}

struct PlatformBreakdownView: View {
    let breakdown: PlatformBreakdown
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Platform Breakdown")
                .font(.headline)
            
            HStack {
                PlatformRow(
                    icon: "iphone",
                    name: "iPhone/iPad",
                    minutes: breakdown.ios
                )
                
                Divider()
                
                PlatformRow(
                    icon: "laptopcomputer",
                    name: "MacBook",
                    minutes: breakdown.macos
                )
            }
        }
        .padding()
        .background(Color(.systemBackground))
        .cornerRadius(12)
        .shadow(radius: 2)
    }
}

struct PlatformRow: View {
    let icon: String
    let name: String
    let minutes: Int
    
    var body: some View {
        VStack {
            Image(systemName: icon)
                .font(.largeTitle)
                .foregroundColor(.blue)
            
            Text(name)
                .font(.caption)
                .foregroundColor(.secondary)
            
            Text(formattedDuration)
                .font(.headline)
        }
        .frame(maxWidth: .infinity)
    }
    
    private var formattedDuration: String {
        let hours = Double(minutes) / 60.0
        return String(format: "%.1fh", hours)
    }
}

Configuration

// Config.swift
enum Config {
    static var apiBaseURL: String {
        #if DEBUG
        return "http://localhost:3000"
        #else
        return "https://your-api.railway.app"
        #endif
    }
    
    static let backgroundTaskIdentifier = "com.dreamlauncher.lite.sync"
    static let syncInterval: TimeInterval = 4 * 60 * 60 // 4 hours
}

Total Line Count Estimate

Compare to old app: 10,000+ lines

90% reduction in code complexity!