From 2f59f2aaf8dedcdc621b6571248726bb72961c08 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 7 Feb 2026 10:58:16 -0600 Subject: [PATCH] modern ios26 Signed-off-by: Matt Bruce --- .../Features/Alarms/Models/Alarm.swift | 4 +- .../Alarms/Services/AlarmKitService.swift | 17 +-- .../Alarms/Services/AlarmService.swift | 16 +-- .../Alarms/State/AlarmViewModel.swift | 2 +- .../Features/Alarms/Views/AlarmView.swift | 4 + .../Views/Components/AlarmRowView.swift | 1 + .../Views/Components/EmptyAlarmsView.swift | 1 + .../Components/TimeUntilAlarmSection.swift | 4 +- .../Features/Alarms/Views/EditAlarmView.swift | 16 +-- .../Clock/Services/BatteryService.swift | 40 +++--- .../Features/Clock/State/ClockViewModel.swift | 124 ++++++++---------- .../Views/Components/BatteryOverlayView.swift | 4 + .../Views/Components/DateOverlayView.swift | 28 +--- .../Views/Components/NextAlarmOverlay.swift | 5 +- .../Views/Components/NoiseMiniPlayer.swift | 2 + .../Settings/AdvancedDisplaySection.swift | 2 + .../Components/Settings/TimePickerView.swift | 7 +- .../Views/Components/SoundCategoryView.swift | 3 +- .../Components/OnboardingWelcomePage.swift | 8 +- .../Shared/Utilities/KeepAwakePrompt.swift | 2 + .../Utilities/KeepAwakePromptState.swift | 2 +- 21 files changed, 134 insertions(+), 158 deletions(-) diff --git a/TheNoiseClock/Features/Alarms/Models/Alarm.swift b/TheNoiseClock/Features/Alarms/Models/Alarm.swift index b18be21..6958676 100644 --- a/TheNoiseClock/Features/Alarms/Models/Alarm.swift +++ b/TheNoiseClock/Features/Alarms/Models/Alarm.swift @@ -56,8 +56,6 @@ struct Alarm: Identifiable, Codable, Equatable { } func formattedTime() -> String { - let formatter = DateFormatter() - formatter.timeStyle = .short - return formatter.string(from: time) + time.formatted(date: .omitted, time: .shortened) } } diff --git a/TheNoiseClock/Features/Alarms/Services/AlarmKitService.swift b/TheNoiseClock/Features/Alarms/Services/AlarmKitService.swift index 1bc4d0e..5a9e4f6 100644 --- a/TheNoiseClock/Features/Alarms/Services/AlarmKitService.swift +++ b/TheNoiseClock/Features/Alarms/Services/AlarmKitService.swift @@ -264,11 +264,7 @@ final class AlarmKitService { /// Log available sound files in Library/Sounds for debugging private func logLibrarySounds() { - guard let libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first else { - return - } - - let soundsDirectory = libraryURL.appendingPathComponent("Sounds") + let soundsDirectory = URL.libraryDirectory.appendingPathComponent("Sounds") Design.debugLog("[alarmkit] ========== LIBRARY/SOUNDS FILES ==========") do { @@ -374,15 +370,14 @@ final class AlarmKitService { /// Create an AlarmKit schedule from an Alarm model. private func createSchedule(for alarm: Alarm) -> AlarmKit.Alarm.Schedule { // Log the raw alarm time - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z" - Design.debugLog("[alarmkit] Raw alarm.time: \(formatter.string(from: alarm.time))") + let debugFormat = Date.FormatStyle.dateTime.year().month().day().hour().minute().second().timeZone() + Design.debugLog("[alarmkit] Raw alarm.time: \(alarm.time.formatted(debugFormat))") // Calculate the next trigger time let triggerDate = alarm.nextTriggerTime() - Design.debugLog("[alarmkit] Next trigger date: \(formatter.string(from: triggerDate))") - Design.debugLog("[alarmkit] Current time: \(formatter.string(from: Date.now))") + Design.debugLog("[alarmkit] Next trigger date: \(triggerDate.formatted(debugFormat))") + Design.debugLog("[alarmkit] Current time: \(Date.now.formatted(debugFormat))") let secondsUntil = triggerDate.timeIntervalSinceNow let minutesUntil = secondsUntil / 60 @@ -396,7 +391,7 @@ final class AlarmKitService { // Use fixed schedule for one-time alarms let schedule = AlarmKit.Alarm.Schedule.fixed(triggerDate) - Design.debugLog("[alarmkit] Schedule created: fixed at \(formatter.string(from: triggerDate))") + Design.debugLog("[alarmkit] Schedule created: fixed at \(triggerDate.formatted(debugFormat))") return schedule } } diff --git a/TheNoiseClock/Features/Alarms/Services/AlarmService.swift b/TheNoiseClock/Features/Alarms/Services/AlarmService.swift index 6a61e92..626080f 100644 --- a/TheNoiseClock/Features/Alarms/Services/AlarmService.swift +++ b/TheNoiseClock/Features/Alarms/Services/AlarmService.swift @@ -16,7 +16,7 @@ import Bedrock /// Service for managing alarm persistence. /// Alarm scheduling is handled by AlarmKitService. @Observable -class AlarmService { +final class AlarmService { // MARK: - Singleton static let shared = AlarmService() @@ -24,7 +24,7 @@ class AlarmService { // MARK: - Properties private(set) var alarms: [Alarm] = [] private var alarmLookup: [UUID: Int] = [:] - private var persistenceWorkItem: DispatchWorkItem? + private var persistenceTask: Task? // MARK: - Initialization init() { @@ -87,20 +87,16 @@ class AlarmService { } private func saveAlarms() { - persistenceWorkItem?.cancel() + persistenceTask?.cancel() let alarmsSnapshot = self.alarms - let work = DispatchWorkItem { + persistenceTask = Task { @MainActor in + try? await Task.sleep(for: .seconds(AppConstants.PersistenceDelays.alarms)) + guard !Task.isCancelled else { return } if let encoded = try? JSONEncoder().encode(alarmsSnapshot) { UserDefaults.standard.set(encoded, forKey: AppConstants.StorageKeys.savedAlarms) } } - persistenceWorkItem = work - - DispatchQueue.main.asyncAfter( - deadline: .now() + AppConstants.PersistenceDelays.alarms, - execute: work - ) } private func loadAlarms() { diff --git a/TheNoiseClock/Features/Alarms/State/AlarmViewModel.swift b/TheNoiseClock/Features/Alarms/State/AlarmViewModel.swift index 536e083..705aabf 100644 --- a/TheNoiseClock/Features/Alarms/State/AlarmViewModel.swift +++ b/TheNoiseClock/Features/Alarms/State/AlarmViewModel.swift @@ -14,7 +14,7 @@ import Observation /// AlarmKit provides alarms that cut through Focus modes and silent mode, /// with built-in Live Activity countdown and system alarm UI. @Observable -class AlarmViewModel { +final class AlarmViewModel { // MARK: - Properties private let alarmService: AlarmService diff --git a/TheNoiseClock/Features/Alarms/Views/AlarmView.swift b/TheNoiseClock/Features/Alarms/Views/AlarmView.swift index 136f07b..8654f21 100644 --- a/TheNoiseClock/Features/Alarms/Views/AlarmView.swift +++ b/TheNoiseClock/Features/Alarms/Views/AlarmView.swift @@ -92,6 +92,7 @@ struct AlarmView: View { } label: { Image(systemName: "plus") .font(.title2) + .symbolEffect(.bounce, value: showAddAlarm) } } } @@ -106,13 +107,16 @@ struct AlarmView: View { viewModel: viewModel, isPresented: $showAddAlarm ) + .presentationCornerRadius(Design.CornerRadius.xxLarge) } .sheet(item: $selectedAlarmForEdit) { alarm in EditAlarmView( viewModel: viewModel, alarm: alarm ) + .presentationCornerRadius(Design.CornerRadius.xxLarge) } + .sensoryFeedback(.impact(flexibility: .soft), trigger: showAddAlarm) } // MARK: - Private Methods diff --git a/TheNoiseClock/Features/Alarms/Views/Components/AlarmRowView.swift b/TheNoiseClock/Features/Alarms/Views/Components/AlarmRowView.swift index 4998c5f..e446c8b 100644 --- a/TheNoiseClock/Features/Alarms/Views/Components/AlarmRowView.swift +++ b/TheNoiseClock/Features/Alarms/Views/Components/AlarmRowView.swift @@ -41,6 +41,7 @@ struct AlarmRowView: View { Image(systemName: "exclamationmark.triangle.fill") .font(.caption2) .foregroundStyle(AppStatus.warning) + .symbolEffect(.pulse, options: .repeating) Text("Foreground only for full alarm sound") .font(.caption2) .foregroundStyle(AppTextColors.tertiary) diff --git a/TheNoiseClock/Features/Alarms/Views/Components/EmptyAlarmsView.swift b/TheNoiseClock/Features/Alarms/Views/Components/EmptyAlarmsView.swift index 86aa861..2fd21f9 100644 --- a/TheNoiseClock/Features/Alarms/Views/Components/EmptyAlarmsView.swift +++ b/TheNoiseClock/Features/Alarms/Views/Components/EmptyAlarmsView.swift @@ -47,6 +47,7 @@ struct EmptyAlarmsView: View { Button(action: onAddAlarm) { HStack { Image(systemName: "plus.circle.fill") + .symbolEffect(.bounce, options: .nonRepeating) Text("Add Your First Alarm") } .typography(.bodyEmphasis) diff --git a/TheNoiseClock/Features/Alarms/Views/Components/TimeUntilAlarmSection.swift b/TheNoiseClock/Features/Alarms/Views/Components/TimeUntilAlarmSection.swift index 63801ff..f0cdf38 100644 --- a/TheNoiseClock/Features/Alarms/Views/Components/TimeUntilAlarmSection.swift +++ b/TheNoiseClock/Features/Alarms/Views/Components/TimeUntilAlarmSection.swift @@ -71,9 +71,7 @@ struct TimeUntilAlarmSection: View { } else if calendar.isDateInTomorrow(alarmTime) { return "Tomorrow" } else { - let formatter = DateFormatter() - formatter.dateFormat = "EEEE" - return formatter.string(from: alarmTime) + return alarmTime.formatted(.dateTime.weekday(.wide)) } } } diff --git a/TheNoiseClock/Features/Alarms/Views/EditAlarmView.swift b/TheNoiseClock/Features/Alarms/Views/EditAlarmView.swift index 11dbb62..6cad965 100644 --- a/TheNoiseClock/Features/Alarms/Views/EditAlarmView.swift +++ b/TheNoiseClock/Features/Alarms/Views/EditAlarmView.swift @@ -67,13 +67,13 @@ struct EditAlarmView: View { NavigationLink(destination: LabelEditView(label: $alarmLabel)) { HStack { Image(systemName: "textformat") - .foregroundColor(AppAccent.primary) + .foregroundStyle(AppAccent.primary) .frame(width: 24) Text("Label") .foregroundStyle(AppTextColors.primary) Spacer() Text(alarmLabel) - .foregroundColor(AppTextColors.secondary) + .foregroundStyle(AppTextColors.secondary) } } .listRowBackground(AppSurface.card) @@ -82,13 +82,13 @@ struct EditAlarmView: View { NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) { HStack { Image(systemName: "message") - .foregroundColor(AppAccent.primary) + .foregroundStyle(AppAccent.primary) .frame(width: 24) Text("Message") .foregroundStyle(AppTextColors.primary) Spacer() Text(notificationMessage) - .foregroundColor(AppTextColors.secondary) + .foregroundStyle(AppTextColors.secondary) .lineLimit(1) } } @@ -98,13 +98,13 @@ struct EditAlarmView: View { NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) { HStack { Image(systemName: "music.note") - .foregroundColor(AppAccent.primary) + .foregroundStyle(AppAccent.primary) .frame(width: 24) Text("Sound") .foregroundStyle(AppTextColors.primary) Spacer() Text(getSoundDisplayName(selectedSoundName)) - .foregroundColor(AppTextColors.secondary) + .foregroundStyle(AppTextColors.secondary) } } .listRowBackground(AppSurface.card) @@ -113,13 +113,13 @@ struct EditAlarmView: View { NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) { HStack { Image(systemName: "clock.arrow.circlepath") - .foregroundColor(AppAccent.primary) + .foregroundStyle(AppAccent.primary) .frame(width: 24) Text("Snooze") .foregroundStyle(AppTextColors.primary) Spacer() Text("for \(snoozeDuration) min") - .foregroundColor(AppTextColors.secondary) + .foregroundStyle(AppTextColors.secondary) } } .listRowBackground(AppSurface.card) diff --git a/TheNoiseClock/Features/Clock/Services/BatteryService.swift b/TheNoiseClock/Features/Clock/Services/BatteryService.swift index ee529b3..6ee2794 100644 --- a/TheNoiseClock/Features/Clock/Services/BatteryService.swift +++ b/TheNoiseClock/Features/Clock/Services/BatteryService.swift @@ -6,12 +6,12 @@ // import Foundation -import Combine import UIKit /// Service for monitoring device battery level and state @Observable -class BatteryService { +@MainActor +final class BatteryService { // MARK: - Properties static let shared = BatteryService() @@ -19,11 +19,11 @@ class BatteryService { var batteryLevel: Int = 100 var isCharging: Bool = false - @ObservationIgnored private var cancellables = Set() + @ObservationIgnored private var monitoringTask: Task? // MARK: - Initialization private init() { - setupBatteryMonitoring() + startNotificationMonitoring() } // MARK: - Public Methods @@ -41,22 +41,24 @@ class BatteryService { } // MARK: - Private Methods - private func setupBatteryMonitoring() { - #if canImport(UIKit) - // Listen for battery level changes - NotificationCenter.default.publisher(for: UIDevice.batteryLevelDidChangeNotification) - .sink { [weak self] _ in - self?.updateBatteryInfo() + private func startNotificationMonitoring() { + monitoringTask = Task { [weak self] in + await withTaskGroup(of: Void.self) { group in + // Monitor battery level changes + group.addTask { @MainActor [weak self] in + for await _ in NotificationCenter.default.notifications(named: UIDevice.batteryLevelDidChangeNotification) { + self?.updateBatteryInfo() + } + } + + // Monitor battery state changes + group.addTask { @MainActor [weak self] in + for await _ in NotificationCenter.default.notifications(named: UIDevice.batteryStateDidChangeNotification) { + self?.updateBatteryInfo() + } + } } - .store(in: &cancellables) - - // Listen for battery state changes - NotificationCenter.default.publisher(for: UIDevice.batteryStateDidChangeNotification) - .sink { [weak self] _ in - self?.updateBatteryInfo() - } - .store(in: &cancellables) - #endif + } } private func updateBatteryInfo() { diff --git a/TheNoiseClock/Features/Clock/State/ClockViewModel.swift b/TheNoiseClock/Features/Clock/State/ClockViewModel.swift index 85083dc..2948cc9 100644 --- a/TheNoiseClock/Features/Clock/State/ClockViewModel.swift +++ b/TheNoiseClock/Features/Clock/State/ClockViewModel.swift @@ -6,7 +6,6 @@ // import Foundation -import Combine import Observation import AudioPlaybackKit import SwiftUI @@ -14,6 +13,7 @@ import Bedrock /// ViewModel for clock display and management @Observable +@MainActor final class ClockViewModel { // MARK: - Properties @@ -27,15 +27,13 @@ final class ClockViewModel { // Ambient light service private let ambientLightService = AmbientLightService.shared - // Timer management - private var secondTimer: Timer.TimerPublisher? - private var minuteTimer: Timer.TimerPublisher? - private var secondCancellable: AnyCancellable? - private var minuteCancellable: AnyCancellable? - private var styleObserver: NSObjectProtocol? + // Async task management (replaces Combine publishers) + @ObservationIgnored private var secondTimerTask: Task? + @ObservationIgnored private var minuteTimerTask: Task? + @ObservationIgnored private var styleObserverTask: Task? // Persistence - private var persistenceWorkItem: DispatchWorkItem? + @ObservationIgnored private var persistenceTask: Task? private var styleJSON: Data { get { UserDefaults.standard.data(forKey: ClockStyle.appStorageKey) ?? { @@ -57,11 +55,10 @@ final class ClockViewModel { } deinit { - stopTimers() - stopAmbientLightMonitoring() - if let styleObserver { - NotificationCenter.default.removeObserver(styleObserver) - } + secondTimerTask?.cancel() + minuteTimerTask?.cancel() + styleObserverTask?.cancel() + persistenceTask?.cancel() } // MARK: - Public Interface @@ -153,82 +150,81 @@ final class ClockViewModel { } private func observeStyleUpdates() { - styleObserver = NotificationCenter.default.addObserver( - forName: .clockStyleDidUpdate, - object: nil, - queue: .main - ) { [weak self] _ in - self?.loadStyle() - self?.updateTimersIfNeeded() - self?.updateWakeLockState() - self?.updateBrightness() + styleObserverTask = Task { [weak self] in + for await _ in NotificationCenter.default.notifications(named: .clockStyleDidUpdate) { + guard let self else { break } + self.loadStyle() + self.updateTimersIfNeeded() + self.updateWakeLockState() + self.updateBrightness() + } } } func saveStyle() { - persistenceWorkItem?.cancel() + persistenceTask?.cancel() - let work = DispatchWorkItem { - if let data = try? JSONEncoder().encode(self.style) { + let style = self.style + persistenceTask = Task { + try? await Task.sleep(for: .seconds(AppConstants.PersistenceDelays.clockStyle)) + guard !Task.isCancelled else { return } + if let data = try? JSONEncoder().encode(style) { self.styleJSON = data } } - persistenceWorkItem = work - - DispatchQueue.main.asyncAfter( - deadline: .now() + AppConstants.PersistenceDelays.clockStyle, - execute: work - ) } private func setupTimers() { - // Always need minute timer for color randomization - if minuteTimer == nil { - minuteTimer = Timer.publish(every: AppConstants.TimerIntervals.minute, on: .main, in: .common) - minuteCancellable = minuteTimer?.autoconnect().sink { _ in + // Always need minute timer for color randomization and night mode checks + startMinuteTimer() + + // Only create second timer if seconds are shown + if style.showSeconds { + startSecondTimer() + } + } + + private func startMinuteTimer() { + minuteTimerTask?.cancel() + minuteTimerTask = Task { [weak self] in + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(AppConstants.TimerIntervals.minute)) + guard !Task.isCancelled, let self else { break } + if self.style.randomizeColor { self.style.digitColorHex = Color.randomBrightColorHex() self.saveStyle() - self.updateBrightness() // Update brightness when color changes + self.updateBrightness() } // Check for night mode state changes (scheduled night mode) - // Force a UI update by updating currentTime slightly self.currentTime = Date() - - // Update brightness if night mode is active self.updateBrightness() } } - - // Only create second timer if seconds are shown - if style.showSeconds && secondTimer == nil { - secondTimer = Timer.publish(every: AppConstants.TimerIntervals.second, on: .main, in: .common) - secondCancellable = secondTimer?.autoconnect().sink { now in - self.currentTime = now + } + + private func startSecondTimer() { + secondTimerTask?.cancel() + secondTimerTask = Task { [weak self] in + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(AppConstants.TimerIntervals.second)) + guard !Task.isCancelled, let self else { break } + self.currentTime = Date() } } } - private func stopTimers() { - secondCancellable?.cancel() - minuteCancellable?.cancel() - secondCancellable = nil - minuteCancellable = nil - secondTimer = nil - minuteTimer = nil + private func stopSecondTimer() { + secondTimerTask?.cancel() + secondTimerTask = nil } private func updateTimersIfNeeded() { - if style.showSeconds && secondTimer == nil { - secondTimer = Timer.publish(every: AppConstants.TimerIntervals.second, on: .main, in: .common) - secondCancellable = secondTimer?.autoconnect().sink { now in - self.currentTime = now - } - } else if !style.showSeconds && secondTimer != nil { - secondCancellable?.cancel() - secondCancellable = nil - secondTimer = nil + if style.showSeconds && secondTimerTask == nil { + startSecondTimer() + } else if !style.showSeconds && secondTimerTask != nil { + stopSecondTimer() } } @@ -253,16 +249,10 @@ final class ClockViewModel { // Set up callback to respond to brightness changes ambientLightService.onBrightnessChange = { [weak self] in - //Design.debugLog("[brightness] ClockViewModel: Received brightness change notification") self?.updateBrightness() } } - /// Stop ambient light monitoring - private func stopAmbientLightMonitoring() { - ambientLightService.stopMonitoring() - } - /// Update brightness based on color theme and night mode settings private func updateBrightness() { if style.autoBrightness { diff --git a/TheNoiseClock/Features/Clock/Views/Components/BatteryOverlayView.swift b/TheNoiseClock/Features/Clock/Views/Components/BatteryOverlayView.swift index d61a961..1576f1d 100644 --- a/TheNoiseClock/Features/Clock/Views/Components/BatteryOverlayView.swift +++ b/TheNoiseClock/Features/Clock/Views/Components/BatteryOverlayView.swift @@ -25,8 +25,12 @@ struct BatteryOverlayView: View { HStack(spacing: 4) { Image(systemName: batteryIcon) .foregroundStyle(batteryColor) + .symbolEffect(.pulse, options: .repeating, isActive: isCharging) + .contentTransition(.symbolEffect(.replace)) Text("\(batteryLevel)%") .foregroundStyle(color) + .contentTransition(.numericText()) + .animation(.snappy(duration: 0.3), value: batteryLevel) } .opacity(clamped) .font(.callout.weight(.semibold)) diff --git a/TheNoiseClock/Features/Clock/Views/Components/DateOverlayView.swift b/TheNoiseClock/Features/Clock/Views/Components/DateOverlayView.swift index 206af64..2ff83c1 100644 --- a/TheNoiseClock/Features/Clock/Views/Components/DateOverlayView.swift +++ b/TheNoiseClock/Features/Clock/Views/Components/DateOverlayView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import Combine /// Component for displaying date overlay struct DateOverlayView: View { @@ -17,8 +16,6 @@ struct DateOverlayView: View { let dateFormat: String @State private var dateString: String = "" - @State private var minuteTimer: Timer.TimerPublisher? - @State private var minuteCancellable: AnyCancellable? // MARK: - Body var body: some View { @@ -30,31 +27,20 @@ struct DateOverlayView: View { .font(.callout.weight(.semibold)) .onAppear { updateDate() - startMinuteUpdates() - } - .onDisappear { - stopMinuteUpdates() } .onChange(of: dateFormat) { _, _ in updateDate() } + .task { + // Periodically update the date every minute + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(AppConstants.TimerIntervals.minute)) + updateDate() + } + } } // MARK: - Private Methods - private func startMinuteUpdates() { - let pub = Timer.publish(every: AppConstants.TimerIntervals.minute, on: .main, in: .common) - minuteTimer = pub - minuteCancellable = pub.autoconnect().sink { _ in - updateDate() - } - } - - private func stopMinuteUpdates() { - minuteCancellable?.cancel() - minuteCancellable = nil - minuteTimer = nil - } - private func updateDate() { dateString = Date().formattedForOverlay(format: dateFormat) } diff --git a/TheNoiseClock/Features/Clock/Views/Components/NextAlarmOverlay.swift b/TheNoiseClock/Features/Clock/Views/Components/NextAlarmOverlay.swift index e1361d2..7c4b0e7 100644 --- a/TheNoiseClock/Features/Clock/Views/Components/NextAlarmOverlay.swift +++ b/TheNoiseClock/Features/Clock/Views/Components/NextAlarmOverlay.swift @@ -18,9 +18,7 @@ struct NextAlarmOverlay: View { private var alarmString: String { guard let time = alarmTime else { return "" } - let formatter = DateFormatter() - formatter.timeStyle = .short - return formatter.string(from: time) + return time.formatted(date: .omitted, time: .shortened) } // MARK: - Body @@ -30,6 +28,7 @@ struct NextAlarmOverlay: View { HStack(spacing: Design.Spacing.xxSmall) { Image(systemName: "alarm.fill") .font(.caption) + .symbolEffect(.bounce, value: alarmTime) Text(alarmString) .typography(.calloutEmphasis) diff --git a/TheNoiseClock/Features/Clock/Views/Components/NoiseMiniPlayer.swift b/TheNoiseClock/Features/Clock/Views/Components/NoiseMiniPlayer.swift index b8c130c..d6d87eb 100644 --- a/TheNoiseClock/Features/Clock/Views/Components/NoiseMiniPlayer.swift +++ b/TheNoiseClock/Features/Clock/Views/Components/NoiseMiniPlayer.swift @@ -26,12 +26,14 @@ struct NoiseMiniPlayer: View { HStack(spacing: Design.Spacing.small) { Button(action: onToggle) { Image(systemName: isPlaying ? "pause.fill" : "play.fill") + .contentTransition(.symbolEffect(.replace)) .font(.system(size: 14, weight: .bold)) .foregroundStyle(.white) .frame(width: 28, height: 28) .background(AppAccent.primary.opacity(0.8)) .clipShape(Circle()) } + .sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying) VStack(alignment: .leading, spacing: 0) { Text(isPlaying ? "Playing" : "Paused") diff --git a/TheNoiseClock/Features/Clock/Views/Components/Settings/AdvancedDisplaySection.swift b/TheNoiseClock/Features/Clock/Views/Components/Settings/AdvancedDisplaySection.swift index 8feaca3..9873e21 100644 --- a/TheNoiseClock/Features/Clock/Views/Components/Settings/AdvancedDisplaySection.swift +++ b/TheNoiseClock/Features/Clock/Views/Components/Settings/AdvancedDisplaySection.swift @@ -54,6 +54,8 @@ struct AdvancedDisplaySection: View { Text("\(Int(style.effectiveBrightness * 100))%") .font(.subheadline) .foregroundStyle(AppTextColors.secondary) + .contentTransition(.numericText()) + .animation(.snappy(duration: 0.3), value: style.effectiveBrightness) } .padding(.vertical, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium) diff --git a/TheNoiseClock/Features/Clock/Views/Components/Settings/TimePickerView.swift b/TheNoiseClock/Features/Clock/Views/Components/Settings/TimePickerView.swift index 79e098e..8967d6b 100644 --- a/TheNoiseClock/Features/Clock/Views/Components/Settings/TimePickerView.swift +++ b/TheNoiseClock/Features/Clock/Views/Components/Settings/TimePickerView.swift @@ -49,8 +49,9 @@ struct TimePickerView: View { } private func updateStringFromTime(_ time: Date) { - let formatter = DateFormatter() - formatter.dateFormat = "HH:mm" - timeString = formatter.string(from: time) + let calendar = Calendar.current + let hour = calendar.component(.hour, from: time) + let minute = calendar.component(.minute, from: time) + timeString = String(format: "%02d:%02d", hour, minute) } } diff --git a/TheNoiseClock/Features/Noise/Views/Components/SoundCategoryView.swift b/TheNoiseClock/Features/Noise/Views/Components/SoundCategoryView.swift index 5cdd6d5..7fe4af1 100644 --- a/TheNoiseClock/Features/Noise/Views/Components/SoundCategoryView.swift +++ b/TheNoiseClock/Features/Noise/Views/Components/SoundCategoryView.swift @@ -81,7 +81,7 @@ struct SoundCategoryView: View { } private var categoryTabs: some View { - ScrollView(.horizontal, showsIndicators: false) { + ScrollView(.horizontal) { HStack(spacing: Design.Spacing.small) { ForEach(categories) { category in CategoryTab( @@ -95,6 +95,7 @@ struct SoundCategoryView: View { } .padding(.horizontal, Design.Spacing.medium) } + .scrollIndicators(.hidden) } private var soundGrid: some View { diff --git a/TheNoiseClock/Features/Onboarding/Views/Components/OnboardingWelcomePage.swift b/TheNoiseClock/Features/Onboarding/Views/Components/OnboardingWelcomePage.swift index ebbbcaf..64d4f20 100644 --- a/TheNoiseClock/Features/Onboarding/Views/Components/OnboardingWelcomePage.swift +++ b/TheNoiseClock/Features/Onboarding/Views/Components/OnboardingWelcomePage.swift @@ -56,14 +56,8 @@ struct OnboardingWelcomePage: View { struct OnboardingClockText: View { let date: Date - private static let formatter: DateFormatter = { - let df = DateFormatter() - df.dateFormat = "h:mm" - return df - }() - private var timeString: String { - Self.formatter.string(from: date) + date.formatted(.dateTime.hour(.defaultDigits(amPM: .omitted)).minute(.twoDigits)) } var body: some View { diff --git a/TheNoiseClock/Shared/Utilities/KeepAwakePrompt.swift b/TheNoiseClock/Shared/Utilities/KeepAwakePrompt.swift index d0839b8..130a9ef 100644 --- a/TheNoiseClock/Shared/Utilities/KeepAwakePrompt.swift +++ b/TheNoiseClock/Shared/Utilities/KeepAwakePrompt.swift @@ -17,6 +17,7 @@ struct KeepAwakePrompt: View { Image(systemName: "bolt.fill") .font(.system(size: 36, weight: .semibold)) .foregroundStyle(AppAccent.primary) + .symbolEffect(.bounce, options: .nonRepeating) VStack(spacing: Design.Spacing.small) { Text("Keep Awake for Alarms") @@ -48,6 +49,7 @@ struct KeepAwakePrompt: View { .frame(maxWidth: .infinity) .background(AppSurface.primary) .presentationDetents([.medium]) + .presentationCornerRadius(Design.CornerRadius.xxLarge) } } diff --git a/TheNoiseClock/Shared/Utilities/KeepAwakePromptState.swift b/TheNoiseClock/Shared/Utilities/KeepAwakePromptState.swift index cd08710..b6f7df0 100644 --- a/TheNoiseClock/Shared/Utilities/KeepAwakePromptState.swift +++ b/TheNoiseClock/Shared/Utilities/KeepAwakePromptState.swift @@ -9,7 +9,7 @@ import Foundation import Observation @Observable -class KeepAwakePromptState { +final class KeepAwakePromptState { var isPresented = false private var hasShownThisSession = false