checkpoint
This commit is contained in:
63
01-assess-appdelegate-responsibilities.md
Normal file
63
01-assess-appdelegate-responsibilities.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# AppDelegate Responsibilities Assessment
|
||||
|
||||
## Overview
|
||||
This document assesses the responsibilities currently handled by the AppDelegate in the Gaze application, identifying the core functions and potential areas for improvement.
|
||||
|
||||
## Current Responsibilities
|
||||
|
||||
### 1. Application Lifecycle Management
|
||||
- Handles `applicationDidFinishLaunching` to initialize app state
|
||||
- Manages `applicationWillTerminate` for cleanup
|
||||
- Sets up system lifecycle observers (sleep/wake notifications)
|
||||
|
||||
### 2. Service Initialization
|
||||
- Initializes the TimerEngine
|
||||
- Sets up smart mode services (FullscreenDetectionService, IdleMonitoringService, UsageTrackingService)
|
||||
- Configures update manager after onboarding completion
|
||||
|
||||
### 3. Settings Management
|
||||
- Observes settings changes to start/stop timers appropriately
|
||||
- Handles onboarding state management
|
||||
- Manages Smart Mode settings observation
|
||||
|
||||
### 4. User Interface Management
|
||||
- Displays onboarding at launch if needed
|
||||
- Shows reminder windows (overlay and subtle)
|
||||
- Manages settings and onboarding windows through WindowManager
|
||||
- Handles menu dismissal logic for proper UI flow
|
||||
|
||||
### 5. Timer State Management
|
||||
- Starts timers when onboarding is complete
|
||||
- Handles system sleep/wake events
|
||||
- Observes timer state changes to update UI
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Positive Aspects:
|
||||
- Clear separation of concerns with service container pattern
|
||||
- Dependency injection allows for testing
|
||||
- Lifecycle management is centralized
|
||||
- Window management is abstracted through protocol
|
||||
|
||||
### Potential Issues:
|
||||
- AppDelegate is handling too many responsibilities (service coordination, UI management, lifecycle)
|
||||
- Direct dependency on NSWorkspace notifications instead of using more structured event handling
|
||||
- Tight coupling between multiple services and the AppDelegate
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Reduce AppDelegate Responsibilities**:
|
||||
- Move timer state change handling to TimerEngine or a dedicated timer manager
|
||||
- Extract window management logic into separate components
|
||||
- Consider delegating system lifecycle handling to dedicated observers
|
||||
|
||||
2. **Improve Modularity**:
|
||||
- Create a dedicated service coordinator that handles inter-service communication
|
||||
- Implement a more structured event system for state changes instead of direct observing
|
||||
|
||||
3. **Enhance Testability**:
|
||||
- The current dependency injection approach is good, but could be made even more flexible
|
||||
- Add more granular mocking capabilities for individual services
|
||||
|
||||
## Conclusion
|
||||
While the AppDelegate currently fulfills its role in managing application lifecycle and coordinating services, it's handling too many responsibilities that should ideally be distributed among specialized components. This makes the code harder to test and maintain.
|
||||
64
Gaze/Services/ReminderManager.swift
Normal file
64
Gaze/Services/ReminderManager.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// ReminderManager.swift
|
||||
// Gaze
|
||||
//
|
||||
// Manages reminder triggering and dismissal logic for timers.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class ReminderManager: ObservableObject {
|
||||
@Published var activeReminder: ReminderEvent?
|
||||
|
||||
private let settingsProvider: any SettingsProviding
|
||||
private var enforceModeService: EnforceModeService?
|
||||
private var timerEngine: TimerEngine?
|
||||
|
||||
init(
|
||||
settingsProvider: any SettingsProviding,
|
||||
enforceModeService: EnforceModeService? = nil
|
||||
) {
|
||||
self.settingsProvider = settingsProvider
|
||||
self.enforceModeService = enforceModeService ?? EnforceModeService.shared
|
||||
}
|
||||
|
||||
func setTimerEngine(_ engine: TimerEngine) {
|
||||
self.timerEngine = engine
|
||||
}
|
||||
|
||||
func triggerReminder(for identifier: TimerIdentifier) {
|
||||
// Pause only the timer that triggered
|
||||
timerEngine?.pauseTimer(identifier: identifier)
|
||||
|
||||
// Unified approach to handle all timer types - no more special handling
|
||||
switch identifier {
|
||||
case .builtIn(let type):
|
||||
switch type {
|
||||
case .lookAway:
|
||||
activeReminder = .lookAwayTriggered(
|
||||
countdownSeconds: settingsProvider.settings.lookAwayCountdownSeconds)
|
||||
case .blink:
|
||||
activeReminder = .blinkTriggered
|
||||
case .posture:
|
||||
activeReminder = .postureTriggered
|
||||
}
|
||||
case .user(let id):
|
||||
if let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) {
|
||||
activeReminder = .userTimerTriggered(userTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dismissReminder() {
|
||||
guard let reminder = activeReminder else { return }
|
||||
activeReminder = nil
|
||||
|
||||
let identifier = reminder.identifier
|
||||
timerEngine?.skipNext(identifier: identifier)
|
||||
timerEngine?.resumeTimer(identifier: identifier)
|
||||
|
||||
enforceModeService?.handleReminderDismissed()
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,8 @@ final class ServiceContainer {
|
||||
}
|
||||
let engine = TimerEngine(
|
||||
settingsManager: settingsManager,
|
||||
enforceModeService: enforceModeService
|
||||
enforceModeService: enforceModeService,
|
||||
timeProvider: isTestEnvironment ? MockTimeProvider() : SystemTimeProvider()
|
||||
)
|
||||
_timerEngine = engine
|
||||
return engine
|
||||
|
||||
72
Gaze/Services/SmartModeManager.swift
Normal file
72
Gaze/Services/SmartModeManager.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// SmartModeManager.swift
|
||||
// Gaze
|
||||
//
|
||||
// Handles smart mode features like idle detection and fullscreen detection.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class SmartModeManager {
|
||||
private var fullscreenService: FullscreenDetectionService?
|
||||
private var idleService: IdleMonitoringService?
|
||||
private var timerEngine: TimerEngine?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
func setupSmartMode(
|
||||
timerEngine: TimerEngine,
|
||||
fullscreenService: FullscreenDetectionService?,
|
||||
idleService: IdleMonitoringService?
|
||||
) {
|
||||
self.timerEngine = timerEngine
|
||||
self.fullscreenService = fullscreenService
|
||||
self.idleService = idleService
|
||||
|
||||
// Subscribe to fullscreen state changes
|
||||
fullscreenService?.$isFullscreenActive
|
||||
.sink { [weak self] isFullscreen in
|
||||
Task { @MainActor in
|
||||
self?.handleFullscreenChange(isFullscreen: isFullscreen)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Subscribe to idle state changes
|
||||
idleService?.$isIdle
|
||||
.sink { [weak self] isIdle in
|
||||
Task { @MainActor in
|
||||
self?.handleIdleChange(isIdle: isIdle)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func handleFullscreenChange(isFullscreen: Bool) {
|
||||
guard let timerEngine = timerEngine else { return }
|
||||
guard timerEngine.settingsProviderForTesting.settings.smartMode.autoPauseOnFullscreen else { return }
|
||||
|
||||
if isFullscreen {
|
||||
timerEngine.pauseAllTimers(reason: .fullscreen)
|
||||
logInfo("⏸️ Timers paused: fullscreen detected")
|
||||
} else {
|
||||
timerEngine.resumeAllTimers(reason: .fullscreen)
|
||||
logInfo("▶️ Timers resumed: fullscreen exited")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleIdleChange(isIdle: Bool) {
|
||||
guard let timerEngine = timerEngine else { return }
|
||||
guard timerEngine.settingsProviderForTesting.settings.smartMode.autoPauseOnIdle else { return }
|
||||
|
||||
if isIdle {
|
||||
timerEngine.pauseAllTimers(reason: .idle)
|
||||
logInfo("⏸️ Timers paused: user idle")
|
||||
} else {
|
||||
timerEngine.resumeAllTimers(reason: .idle)
|
||||
logInfo("▶️ Timers resumed: user active")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,11 @@ class TimerEngine: ObservableObject {
|
||||
|
||||
private var timerSubscription: AnyCancellable?
|
||||
private let settingsProvider: any SettingsProviding
|
||||
|
||||
// Expose the settings provider for components that need it (like SmartModeManager)
|
||||
var settingsProviderForTesting: any SettingsProviding {
|
||||
return settingsProvider
|
||||
}
|
||||
private var sleepStartTime: Date?
|
||||
|
||||
/// Time provider for deterministic testing (defaults to system time)
|
||||
@@ -103,7 +108,7 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func pauseAllTimers(reason: PauseReason) {
|
||||
func pauseAllTimers(reason: PauseReason) {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.insert(reason)
|
||||
state.isPaused = true
|
||||
@@ -111,7 +116,7 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func resumeAllTimers(reason: PauseReason) {
|
||||
func resumeAllTimers(reason: PauseReason) {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.remove(reason)
|
||||
state.isPaused = !state.pauseReasons.isEmpty
|
||||
@@ -394,57 +399,20 @@ func skipNext(identifier: TimerIdentifier) {
|
||||
return timerStates[identifier]?.isPaused ?? true
|
||||
}
|
||||
|
||||
/// Handles system sleep event
|
||||
/// - Saves current time for elapsed calculation
|
||||
/// - Pauses all active timers
|
||||
// System sleep/wake handling is now managed by SystemSleepManager
|
||||
// This method is kept for compatibility but will be removed in future versions
|
||||
/// Handles system sleep event - deprecated
|
||||
@available(*, deprecated, message: "Use SystemSleepManager instead")
|
||||
func handleSystemSleep() {
|
||||
logDebug("System going to sleep")
|
||||
sleepStartTime = timeProvider.now()
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.insert(.system)
|
||||
state.isPaused = true
|
||||
timerStates[id] = state
|
||||
}
|
||||
logDebug("System going to sleep (deprecated)")
|
||||
// This functionality has been moved to SystemSleepManager
|
||||
}
|
||||
|
||||
/// Handles system wake event
|
||||
/// - Calculates elapsed time during sleep
|
||||
/// - Adjusts remaining time for all active timers
|
||||
/// - Timers that expired during sleep will trigger immediately (1s delay)
|
||||
/// - Resumes all timers
|
||||
/// Handles system wake event - deprecated
|
||||
@available(*, deprecated, message: "Use SystemSleepManager instead")
|
||||
func handleSystemWake() {
|
||||
logDebug("System waking up")
|
||||
guard let sleepStart = sleepStartTime else {
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
sleepStartTime = nil
|
||||
}
|
||||
|
||||
let elapsedSeconds = Int(timeProvider.now().timeIntervalSince(sleepStart))
|
||||
|
||||
guard elapsedSeconds >= 1 else {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.remove(.system)
|
||||
state.isPaused = !state.pauseReasons.isEmpty
|
||||
timerStates[id] = state
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (identifier, state) in timerStates where state.isActive {
|
||||
var updatedState = state
|
||||
updatedState.remainingSeconds = max(0, state.remainingSeconds - elapsedSeconds)
|
||||
|
||||
if updatedState.remainingSeconds <= 0 {
|
||||
updatedState.remainingSeconds = 1
|
||||
}
|
||||
|
||||
updatedState.pauseReasons.remove(.system)
|
||||
updatedState.isPaused = !updatedState.pauseReasons.isEmpty
|
||||
timerStates[identifier] = updatedState
|
||||
}
|
||||
logDebug("System waking up (deprecated)")
|
||||
// This functionality has been moved to SystemSleepManager
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
248
Gaze/Services/TimerManager.swift
Normal file
248
Gaze/Services/TimerManager.swift
Normal file
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// TimerManager.swift
|
||||
// Gaze
|
||||
//
|
||||
// Manages timer creation, state updates, and lifecycle operations.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class TimerManager: ObservableObject {
|
||||
@Published var timerStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
private let settingsProvider: any SettingsProviding
|
||||
private var timerSubscription: AnyCancellable?
|
||||
private let timeProvider: TimeProviding
|
||||
|
||||
init(
|
||||
settingsManager: any SettingsProviding,
|
||||
timeProvider: TimeProviding
|
||||
) {
|
||||
self.settingsProvider = settingsManager
|
||||
self.timeProvider = timeProvider
|
||||
}
|
||||
|
||||
func start() {
|
||||
// If timers are already running, just update configurations without resetting
|
||||
if timerSubscription != nil {
|
||||
updateConfigurations()
|
||||
return
|
||||
}
|
||||
|
||||
// Initial start - create all timer states
|
||||
stop()
|
||||
|
||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
// Add built-in timers (using unified approach)
|
||||
for timerType in TimerType.allCases {
|
||||
let config = settingsProvider.timerConfiguration(for: timerType)
|
||||
if config.enabled {
|
||||
let identifier = TimerIdentifier.builtIn(timerType)
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: config.intervalSeconds,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add user timers (using unified approach)
|
||||
for userTimer in settingsProvider.settings.userTimers where userTimer.enabled {
|
||||
let identifier = TimerIdentifier.user(id: userTimer.id)
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: userTimer.intervalMinutes * 60,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
|
||||
// Assign the entire dictionary at once to trigger @Published
|
||||
timerStates = newStates
|
||||
|
||||
timerSubscription = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.handleTick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
timerSubscription?.cancel()
|
||||
timerSubscription = nil
|
||||
timerStates.removeAll()
|
||||
}
|
||||
|
||||
func updateConfigurations() {
|
||||
// Update configurations from settings
|
||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
// Update built-in timers (using unified approach)
|
||||
for timerType in TimerType.allCases {
|
||||
let config = settingsProvider.timerConfiguration(for: timerType)
|
||||
let identifier = TimerIdentifier.builtIn(timerType)
|
||||
|
||||
if config.enabled {
|
||||
if let existingState = timerStates[identifier] {
|
||||
// Timer exists - check if interval changed
|
||||
if existingState.originalIntervalSeconds != config.intervalSeconds {
|
||||
// Interval changed - reset with new interval
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: config.intervalSeconds,
|
||||
isPaused: existingState.isPaused,
|
||||
isActive: true
|
||||
)
|
||||
} else {
|
||||
// Interval unchanged - keep existing state
|
||||
newStates[identifier] = existingState
|
||||
}
|
||||
} else {
|
||||
// Timer was just enabled - create new state
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: config.intervalSeconds,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
}
|
||||
// If config.enabled is false and timer exists, it will be removed
|
||||
}
|
||||
|
||||
// Update user timers (using unified approach)
|
||||
for userTimer in settingsProvider.settings.userTimers {
|
||||
let identifier = TimerIdentifier.user(id: userTimer.id)
|
||||
let newIntervalSeconds = userTimer.intervalMinutes * 60
|
||||
|
||||
if userTimer.enabled {
|
||||
if let existingState = timerStates[identifier] {
|
||||
// Check if interval changed
|
||||
if existingState.originalIntervalSeconds != newIntervalSeconds {
|
||||
// Interval changed - reset with new interval
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: newIntervalSeconds,
|
||||
isPaused: existingState.isPaused,
|
||||
isActive: true
|
||||
)
|
||||
} else {
|
||||
// Interval unchanged - keep existing state
|
||||
newStates[identifier] = existingState
|
||||
}
|
||||
} else {
|
||||
// New timer - create state
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: newIntervalSeconds,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
}
|
||||
// If timer is disabled, it will be removed
|
||||
}
|
||||
|
||||
// Assign the entire dictionary at once to trigger @Published
|
||||
timerStates = newStates
|
||||
}
|
||||
|
||||
private func handleTick() {
|
||||
for (identifier, state) in timerStates {
|
||||
guard !state.isPaused else { continue }
|
||||
guard state.isActive else { continue }
|
||||
|
||||
if state.targetDate < timeProvider.now() - 3.0 {
|
||||
// Timer has expired but with some grace period
|
||||
continue
|
||||
}
|
||||
|
||||
timerStates[identifier]?.remainingSeconds -= 1
|
||||
|
||||
if let updatedState = timerStates[identifier] {
|
||||
// Update remaining seconds for the timer
|
||||
if updatedState.remainingSeconds <= 0 {
|
||||
// This would normally trigger a reminder in a full implementation,
|
||||
// but we're decomposing it to separate components
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.insert(.manual)
|
||||
state.isPaused = true
|
||||
timerStates[id] = state
|
||||
}
|
||||
}
|
||||
|
||||
func resume() {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.remove(.manual)
|
||||
state.isPaused = !state.pauseReasons.isEmpty
|
||||
timerStates[id] = state
|
||||
}
|
||||
}
|
||||
|
||||
func pauseTimer(identifier: TimerIdentifier) {
|
||||
guard var state = timerStates[identifier] else { return }
|
||||
state.pauseReasons.insert(.manual)
|
||||
state.isPaused = true
|
||||
timerStates[identifier] = state
|
||||
}
|
||||
|
||||
func resumeTimer(identifier: TimerIdentifier) {
|
||||
guard var state = timerStates[identifier] else { return }
|
||||
state.pauseReasons.remove(.manual)
|
||||
state.isPaused = !state.pauseReasons.isEmpty
|
||||
timerStates[identifier] = state
|
||||
}
|
||||
|
||||
func skipNext(identifier: TimerIdentifier) {
|
||||
guard let state = timerStates[identifier] else { return }
|
||||
|
||||
// Unified approach to get interval - no more separate handling for user timers
|
||||
let intervalSeconds = getTimerInterval(for: identifier)
|
||||
|
||||
timerStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: intervalSeconds,
|
||||
isPaused: state.isPaused,
|
||||
isActive: state.isActive
|
||||
)
|
||||
}
|
||||
|
||||
/// Unified way to get interval for any timer type
|
||||
private func getTimerInterval(for identifier: TimerIdentifier) -> Int {
|
||||
switch identifier {
|
||||
case .builtIn(let type):
|
||||
let config = settingsProvider.timerConfiguration(for: type)
|
||||
return config.intervalSeconds
|
||||
case .user(let id):
|
||||
guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) else {
|
||||
return 0
|
||||
}
|
||||
return userTimer.intervalMinutes * 60
|
||||
}
|
||||
}
|
||||
|
||||
func getTimeRemaining(for identifier: TimerIdentifier) -> TimeInterval {
|
||||
guard let state = timerStates[identifier] else { return 0 }
|
||||
return TimeInterval(state.remainingSeconds)
|
||||
}
|
||||
|
||||
func getFormattedTimeRemaining(for identifier: TimerIdentifier) -> String {
|
||||
return getTimeRemaining(for: identifier).formatAsTimerDurationFull()
|
||||
}
|
||||
|
||||
func isTimerPaused(_ identifier: TimerIdentifier) -> Bool {
|
||||
return timerStates[identifier]?.isPaused ?? true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user