modern ios26

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
Matt Bruce 2026-02-07 10:58:16 -06:00
parent 4bf7994e31
commit 2f59f2aaf8
21 changed files with 134 additions and 158 deletions

View File

@ -56,8 +56,6 @@ struct Alarm: Identifiable, Codable, Equatable {
} }
func formattedTime() -> String { func formattedTime() -> String {
let formatter = DateFormatter() time.formatted(date: .omitted, time: .shortened)
formatter.timeStyle = .short
return formatter.string(from: time)
} }
} }

View File

@ -264,11 +264,7 @@ final class AlarmKitService {
/// Log available sound files in Library/Sounds for debugging /// Log available sound files in Library/Sounds for debugging
private func logLibrarySounds() { private func logLibrarySounds() {
guard let libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first else { let soundsDirectory = URL.libraryDirectory.appendingPathComponent("Sounds")
return
}
let soundsDirectory = libraryURL.appendingPathComponent("Sounds")
Design.debugLog("[alarmkit] ========== LIBRARY/SOUNDS FILES ==========") Design.debugLog("[alarmkit] ========== LIBRARY/SOUNDS FILES ==========")
do { do {
@ -374,15 +370,14 @@ final class AlarmKitService {
/// Create an AlarmKit schedule from an Alarm model. /// Create an AlarmKit schedule from an Alarm model.
private func createSchedule(for alarm: Alarm) -> AlarmKit.Alarm.Schedule { private func createSchedule(for alarm: Alarm) -> AlarmKit.Alarm.Schedule {
// Log the raw alarm time // Log the raw alarm time
let formatter = DateFormatter() let debugFormat = Date.FormatStyle.dateTime.year().month().day().hour().minute().second().timeZone()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z" Design.debugLog("[alarmkit] Raw alarm.time: \(alarm.time.formatted(debugFormat))")
Design.debugLog("[alarmkit] Raw alarm.time: \(formatter.string(from: alarm.time))")
// Calculate the next trigger time // Calculate the next trigger time
let triggerDate = alarm.nextTriggerTime() let triggerDate = alarm.nextTriggerTime()
Design.debugLog("[alarmkit] Next trigger date: \(formatter.string(from: triggerDate))") Design.debugLog("[alarmkit] Next trigger date: \(triggerDate.formatted(debugFormat))")
Design.debugLog("[alarmkit] Current time: \(formatter.string(from: Date.now))") Design.debugLog("[alarmkit] Current time: \(Date.now.formatted(debugFormat))")
let secondsUntil = triggerDate.timeIntervalSinceNow let secondsUntil = triggerDate.timeIntervalSinceNow
let minutesUntil = secondsUntil / 60 let minutesUntil = secondsUntil / 60
@ -396,7 +391,7 @@ final class AlarmKitService {
// Use fixed schedule for one-time alarms // Use fixed schedule for one-time alarms
let schedule = AlarmKit.Alarm.Schedule.fixed(triggerDate) 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 return schedule
} }
} }

View File

@ -16,7 +16,7 @@ import Bedrock
/// Service for managing alarm persistence. /// Service for managing alarm persistence.
/// Alarm scheduling is handled by AlarmKitService. /// Alarm scheduling is handled by AlarmKitService.
@Observable @Observable
class AlarmService { final class AlarmService {
// MARK: - Singleton // MARK: - Singleton
static let shared = AlarmService() static let shared = AlarmService()
@ -24,7 +24,7 @@ class AlarmService {
// MARK: - Properties // MARK: - Properties
private(set) var alarms: [Alarm] = [] private(set) var alarms: [Alarm] = []
private var alarmLookup: [UUID: Int] = [:] private var alarmLookup: [UUID: Int] = [:]
private var persistenceWorkItem: DispatchWorkItem? private var persistenceTask: Task<Void, Never>?
// MARK: - Initialization // MARK: - Initialization
init() { init() {
@ -87,20 +87,16 @@ class AlarmService {
} }
private func saveAlarms() { private func saveAlarms() {
persistenceWorkItem?.cancel() persistenceTask?.cancel()
let alarmsSnapshot = self.alarms 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) { if let encoded = try? JSONEncoder().encode(alarmsSnapshot) {
UserDefaults.standard.set(encoded, forKey: AppConstants.StorageKeys.savedAlarms) UserDefaults.standard.set(encoded, forKey: AppConstants.StorageKeys.savedAlarms)
} }
} }
persistenceWorkItem = work
DispatchQueue.main.asyncAfter(
deadline: .now() + AppConstants.PersistenceDelays.alarms,
execute: work
)
} }
private func loadAlarms() { private func loadAlarms() {

View File

@ -14,7 +14,7 @@ import Observation
/// AlarmKit provides alarms that cut through Focus modes and silent mode, /// AlarmKit provides alarms that cut through Focus modes and silent mode,
/// with built-in Live Activity countdown and system alarm UI. /// with built-in Live Activity countdown and system alarm UI.
@Observable @Observable
class AlarmViewModel { final class AlarmViewModel {
// MARK: - Properties // MARK: - Properties
private let alarmService: AlarmService private let alarmService: AlarmService

View File

@ -92,6 +92,7 @@ struct AlarmView: View {
} label: { } label: {
Image(systemName: "plus") Image(systemName: "plus")
.font(.title2) .font(.title2)
.symbolEffect(.bounce, value: showAddAlarm)
} }
} }
} }
@ -106,13 +107,16 @@ struct AlarmView: View {
viewModel: viewModel, viewModel: viewModel,
isPresented: $showAddAlarm isPresented: $showAddAlarm
) )
.presentationCornerRadius(Design.CornerRadius.xxLarge)
} }
.sheet(item: $selectedAlarmForEdit) { alarm in .sheet(item: $selectedAlarmForEdit) { alarm in
EditAlarmView( EditAlarmView(
viewModel: viewModel, viewModel: viewModel,
alarm: alarm alarm: alarm
) )
.presentationCornerRadius(Design.CornerRadius.xxLarge)
} }
.sensoryFeedback(.impact(flexibility: .soft), trigger: showAddAlarm)
} }
// MARK: - Private Methods // MARK: - Private Methods

View File

@ -41,6 +41,7 @@ struct AlarmRowView: View {
Image(systemName: "exclamationmark.triangle.fill") Image(systemName: "exclamationmark.triangle.fill")
.font(.caption2) .font(.caption2)
.foregroundStyle(AppStatus.warning) .foregroundStyle(AppStatus.warning)
.symbolEffect(.pulse, options: .repeating)
Text("Foreground only for full alarm sound") Text("Foreground only for full alarm sound")
.font(.caption2) .font(.caption2)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)

View File

@ -47,6 +47,7 @@ struct EmptyAlarmsView: View {
Button(action: onAddAlarm) { Button(action: onAddAlarm) {
HStack { HStack {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
.symbolEffect(.bounce, options: .nonRepeating)
Text("Add Your First Alarm") Text("Add Your First Alarm")
} }
.typography(.bodyEmphasis) .typography(.bodyEmphasis)

View File

@ -71,9 +71,7 @@ struct TimeUntilAlarmSection: View {
} else if calendar.isDateInTomorrow(alarmTime) { } else if calendar.isDateInTomorrow(alarmTime) {
return "Tomorrow" return "Tomorrow"
} else { } else {
let formatter = DateFormatter() return alarmTime.formatted(.dateTime.weekday(.wide))
formatter.dateFormat = "EEEE"
return formatter.string(from: alarmTime)
} }
} }
} }

View File

@ -67,13 +67,13 @@ struct EditAlarmView: View {
NavigationLink(destination: LabelEditView(label: $alarmLabel)) { NavigationLink(destination: LabelEditView(label: $alarmLabel)) {
HStack { HStack {
Image(systemName: "textformat") Image(systemName: "textformat")
.foregroundColor(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Label") Text("Label")
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text(alarmLabel) Text(alarmLabel)
.foregroundColor(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
} }
.listRowBackground(AppSurface.card) .listRowBackground(AppSurface.card)
@ -82,13 +82,13 @@ struct EditAlarmView: View {
NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) { NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) {
HStack { HStack {
Image(systemName: "message") Image(systemName: "message")
.foregroundColor(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Message") Text("Message")
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text(notificationMessage) Text(notificationMessage)
.foregroundColor(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.lineLimit(1) .lineLimit(1)
} }
} }
@ -98,13 +98,13 @@ struct EditAlarmView: View {
NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) { NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) {
HStack { HStack {
Image(systemName: "music.note") Image(systemName: "music.note")
.foregroundColor(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Sound") Text("Sound")
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text(getSoundDisplayName(selectedSoundName)) Text(getSoundDisplayName(selectedSoundName))
.foregroundColor(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
} }
.listRowBackground(AppSurface.card) .listRowBackground(AppSurface.card)
@ -113,13 +113,13 @@ struct EditAlarmView: View {
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) { NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
HStack { HStack {
Image(systemName: "clock.arrow.circlepath") Image(systemName: "clock.arrow.circlepath")
.foregroundColor(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Snooze") Text("Snooze")
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text("for \(snoozeDuration) min") Text("for \(snoozeDuration) min")
.foregroundColor(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
} }
.listRowBackground(AppSurface.card) .listRowBackground(AppSurface.card)

View File

@ -6,12 +6,12 @@
// //
import Foundation import Foundation
import Combine
import UIKit import UIKit
/// Service for monitoring device battery level and state /// Service for monitoring device battery level and state
@Observable @Observable
class BatteryService { @MainActor
final class BatteryService {
// MARK: - Properties // MARK: - Properties
static let shared = BatteryService() static let shared = BatteryService()
@ -19,11 +19,11 @@ class BatteryService {
var batteryLevel: Int = 100 var batteryLevel: Int = 100
var isCharging: Bool = false var isCharging: Bool = false
@ObservationIgnored private var cancellables = Set<AnyCancellable>() @ObservationIgnored private var monitoringTask: Task<Void, Never>?
// MARK: - Initialization // MARK: - Initialization
private init() { private init() {
setupBatteryMonitoring() startNotificationMonitoring()
} }
// MARK: - Public Methods // MARK: - Public Methods
@ -41,22 +41,24 @@ class BatteryService {
} }
// MARK: - Private Methods // MARK: - Private Methods
private func setupBatteryMonitoring() { private func startNotificationMonitoring() {
#if canImport(UIKit) monitoringTask = Task { [weak self] in
// Listen for battery level changes await withTaskGroup(of: Void.self) { group in
NotificationCenter.default.publisher(for: UIDevice.batteryLevelDidChangeNotification) // Monitor battery level changes
.sink { [weak self] _ in group.addTask { @MainActor [weak self] in
self?.updateBatteryInfo() 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() { private func updateBatteryInfo() {

View File

@ -6,7 +6,6 @@
// //
import Foundation import Foundation
import Combine
import Observation import Observation
import AudioPlaybackKit import AudioPlaybackKit
import SwiftUI import SwiftUI
@ -14,6 +13,7 @@ import Bedrock
/// ViewModel for clock display and management /// ViewModel for clock display and management
@Observable @Observable
@MainActor
final class ClockViewModel { final class ClockViewModel {
// MARK: - Properties // MARK: - Properties
@ -27,15 +27,13 @@ final class ClockViewModel {
// Ambient light service // Ambient light service
private let ambientLightService = AmbientLightService.shared private let ambientLightService = AmbientLightService.shared
// Timer management // Async task management (replaces Combine publishers)
private var secondTimer: Timer.TimerPublisher? @ObservationIgnored private var secondTimerTask: Task<Void, Never>?
private var minuteTimer: Timer.TimerPublisher? @ObservationIgnored private var minuteTimerTask: Task<Void, Never>?
private var secondCancellable: AnyCancellable? @ObservationIgnored private var styleObserverTask: Task<Void, Never>?
private var minuteCancellable: AnyCancellable?
private var styleObserver: NSObjectProtocol?
// Persistence // Persistence
private var persistenceWorkItem: DispatchWorkItem? @ObservationIgnored private var persistenceTask: Task<Void, Never>?
private var styleJSON: Data { private var styleJSON: Data {
get { get {
UserDefaults.standard.data(forKey: ClockStyle.appStorageKey) ?? { UserDefaults.standard.data(forKey: ClockStyle.appStorageKey) ?? {
@ -57,11 +55,10 @@ final class ClockViewModel {
} }
deinit { deinit {
stopTimers() secondTimerTask?.cancel()
stopAmbientLightMonitoring() minuteTimerTask?.cancel()
if let styleObserver { styleObserverTask?.cancel()
NotificationCenter.default.removeObserver(styleObserver) persistenceTask?.cancel()
}
} }
// MARK: - Public Interface // MARK: - Public Interface
@ -153,82 +150,81 @@ final class ClockViewModel {
} }
private func observeStyleUpdates() { private func observeStyleUpdates() {
styleObserver = NotificationCenter.default.addObserver( styleObserverTask = Task { [weak self] in
forName: .clockStyleDidUpdate, for await _ in NotificationCenter.default.notifications(named: .clockStyleDidUpdate) {
object: nil, guard let self else { break }
queue: .main self.loadStyle()
) { [weak self] _ in self.updateTimersIfNeeded()
self?.loadStyle() self.updateWakeLockState()
self?.updateTimersIfNeeded() self.updateBrightness()
self?.updateWakeLockState() }
self?.updateBrightness()
} }
} }
func saveStyle() { func saveStyle() {
persistenceWorkItem?.cancel() persistenceTask?.cancel()
let work = DispatchWorkItem { let style = self.style
if let data = try? JSONEncoder().encode(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 self.styleJSON = data
} }
} }
persistenceWorkItem = work
DispatchQueue.main.asyncAfter(
deadline: .now() + AppConstants.PersistenceDelays.clockStyle,
execute: work
)
} }
private func setupTimers() { private func setupTimers() {
// Always need minute timer for color randomization // Always need minute timer for color randomization and night mode checks
if minuteTimer == nil { startMinuteTimer()
minuteTimer = Timer.publish(every: AppConstants.TimerIntervals.minute, on: .main, in: .common)
minuteCancellable = minuteTimer?.autoconnect().sink { _ in // 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 { if self.style.randomizeColor {
self.style.digitColorHex = Color.randomBrightColorHex() self.style.digitColorHex = Color.randomBrightColorHex()
self.saveStyle() self.saveStyle()
self.updateBrightness() // Update brightness when color changes self.updateBrightness()
} }
// Check for night mode state changes (scheduled night mode) // Check for night mode state changes (scheduled night mode)
// Force a UI update by updating currentTime slightly
self.currentTime = Date() self.currentTime = Date()
// Update brightness if night mode is active
self.updateBrightness() self.updateBrightness()
} }
} }
}
// Only create second timer if seconds are shown
if style.showSeconds && secondTimer == nil { private func startSecondTimer() {
secondTimer = Timer.publish(every: AppConstants.TimerIntervals.second, on: .main, in: .common) secondTimerTask?.cancel()
secondCancellable = secondTimer?.autoconnect().sink { now in secondTimerTask = Task { [weak self] in
self.currentTime = now 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() { private func stopSecondTimer() {
secondCancellable?.cancel() secondTimerTask?.cancel()
minuteCancellable?.cancel() secondTimerTask = nil
secondCancellable = nil
minuteCancellable = nil
secondTimer = nil
minuteTimer = nil
} }
private func updateTimersIfNeeded() { private func updateTimersIfNeeded() {
if style.showSeconds && secondTimer == nil { if style.showSeconds && secondTimerTask == nil {
secondTimer = Timer.publish(every: AppConstants.TimerIntervals.second, on: .main, in: .common) startSecondTimer()
secondCancellable = secondTimer?.autoconnect().sink { now in } else if !style.showSeconds && secondTimerTask != nil {
self.currentTime = now stopSecondTimer()
}
} else if !style.showSeconds && secondTimer != nil {
secondCancellable?.cancel()
secondCancellable = nil
secondTimer = nil
} }
} }
@ -253,16 +249,10 @@ final class ClockViewModel {
// Set up callback to respond to brightness changes // Set up callback to respond to brightness changes
ambientLightService.onBrightnessChange = { [weak self] in ambientLightService.onBrightnessChange = { [weak self] in
//Design.debugLog("[brightness] ClockViewModel: Received brightness change notification")
self?.updateBrightness() self?.updateBrightness()
} }
} }
/// Stop ambient light monitoring
private func stopAmbientLightMonitoring() {
ambientLightService.stopMonitoring()
}
/// Update brightness based on color theme and night mode settings /// Update brightness based on color theme and night mode settings
private func updateBrightness() { private func updateBrightness() {
if style.autoBrightness { if style.autoBrightness {

View File

@ -25,8 +25,12 @@ struct BatteryOverlayView: View {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: batteryIcon) Image(systemName: batteryIcon)
.foregroundStyle(batteryColor) .foregroundStyle(batteryColor)
.symbolEffect(.pulse, options: .repeating, isActive: isCharging)
.contentTransition(.symbolEffect(.replace))
Text("\(batteryLevel)%") Text("\(batteryLevel)%")
.foregroundStyle(color) .foregroundStyle(color)
.contentTransition(.numericText())
.animation(.snappy(duration: 0.3), value: batteryLevel)
} }
.opacity(clamped) .opacity(clamped)
.font(.callout.weight(.semibold)) .font(.callout.weight(.semibold))

View File

@ -6,7 +6,6 @@
// //
import SwiftUI import SwiftUI
import Combine
/// Component for displaying date overlay /// Component for displaying date overlay
struct DateOverlayView: View { struct DateOverlayView: View {
@ -17,8 +16,6 @@ struct DateOverlayView: View {
let dateFormat: String let dateFormat: String
@State private var dateString: String = "" @State private var dateString: String = ""
@State private var minuteTimer: Timer.TimerPublisher?
@State private var minuteCancellable: AnyCancellable?
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
@ -30,31 +27,20 @@ struct DateOverlayView: View {
.font(.callout.weight(.semibold)) .font(.callout.weight(.semibold))
.onAppear { .onAppear {
updateDate() updateDate()
startMinuteUpdates()
}
.onDisappear {
stopMinuteUpdates()
} }
.onChange(of: dateFormat) { _, _ in .onChange(of: dateFormat) { _, _ in
updateDate() updateDate()
} }
.task {
// Periodically update the date every minute
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(AppConstants.TimerIntervals.minute))
updateDate()
}
}
} }
// MARK: - Private Methods // 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() { private func updateDate() {
dateString = Date().formattedForOverlay(format: dateFormat) dateString = Date().formattedForOverlay(format: dateFormat)
} }

View File

@ -18,9 +18,7 @@ struct NextAlarmOverlay: View {
private var alarmString: String { private var alarmString: String {
guard let time = alarmTime else { return "" } guard let time = alarmTime else { return "" }
let formatter = DateFormatter() return time.formatted(date: .omitted, time: .shortened)
formatter.timeStyle = .short
return formatter.string(from: time)
} }
// MARK: - Body // MARK: - Body
@ -30,6 +28,7 @@ struct NextAlarmOverlay: View {
HStack(spacing: Design.Spacing.xxSmall) { HStack(spacing: Design.Spacing.xxSmall) {
Image(systemName: "alarm.fill") Image(systemName: "alarm.fill")
.font(.caption) .font(.caption)
.symbolEffect(.bounce, value: alarmTime)
Text(alarmString) Text(alarmString)
.typography(.calloutEmphasis) .typography(.calloutEmphasis)

View File

@ -26,12 +26,14 @@ struct NoiseMiniPlayer: View {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Button(action: onToggle) { Button(action: onToggle) {
Image(systemName: isPlaying ? "pause.fill" : "play.fill") Image(systemName: isPlaying ? "pause.fill" : "play.fill")
.contentTransition(.symbolEffect(.replace))
.font(.system(size: 14, weight: .bold)) .font(.system(size: 14, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
.frame(width: 28, height: 28) .frame(width: 28, height: 28)
.background(AppAccent.primary.opacity(0.8)) .background(AppAccent.primary.opacity(0.8))
.clipShape(Circle()) .clipShape(Circle())
} }
.sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(isPlaying ? "Playing" : "Paused") Text(isPlaying ? "Playing" : "Paused")

View File

@ -54,6 +54,8 @@ struct AdvancedDisplaySection: View {
Text("\(Int(style.effectiveBrightness * 100))%") Text("\(Int(style.effectiveBrightness * 100))%")
.font(.subheadline) .font(.subheadline)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.contentTransition(.numericText())
.animation(.snappy(duration: 0.3), value: style.effectiveBrightness)
} }
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)

View File

@ -49,8 +49,9 @@ struct TimePickerView: View {
} }
private func updateStringFromTime(_ time: Date) { private func updateStringFromTime(_ time: Date) {
let formatter = DateFormatter() let calendar = Calendar.current
formatter.dateFormat = "HH:mm" let hour = calendar.component(.hour, from: time)
timeString = formatter.string(from: time) let minute = calendar.component(.minute, from: time)
timeString = String(format: "%02d:%02d", hour, minute)
} }
} }

View File

@ -81,7 +81,7 @@ struct SoundCategoryView: View {
} }
private var categoryTabs: some View { private var categoryTabs: some View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal) {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
ForEach(categories) { category in ForEach(categories) { category in
CategoryTab( CategoryTab(
@ -95,6 +95,7 @@ struct SoundCategoryView: View {
} }
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
} }
.scrollIndicators(.hidden)
} }
private var soundGrid: some View { private var soundGrid: some View {

View File

@ -56,14 +56,8 @@ struct OnboardingWelcomePage: View {
struct OnboardingClockText: View { struct OnboardingClockText: View {
let date: Date let date: Date
private static let formatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "h:mm"
return df
}()
private var timeString: String { private var timeString: String {
Self.formatter.string(from: date) date.formatted(.dateTime.hour(.defaultDigits(amPM: .omitted)).minute(.twoDigits))
} }
var body: some View { var body: some View {

View File

@ -17,6 +17,7 @@ struct KeepAwakePrompt: View {
Image(systemName: "bolt.fill") Image(systemName: "bolt.fill")
.font(.system(size: 36, weight: .semibold)) .font(.system(size: 36, weight: .semibold))
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.symbolEffect(.bounce, options: .nonRepeating)
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
Text("Keep Awake for Alarms") Text("Keep Awake for Alarms")
@ -48,6 +49,7 @@ struct KeepAwakePrompt: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(AppSurface.primary) .background(AppSurface.primary)
.presentationDetents([.medium]) .presentationDetents([.medium])
.presentationCornerRadius(Design.CornerRadius.xxLarge)
} }
} }

View File

@ -9,7 +9,7 @@ import Foundation
import Observation import Observation
@Observable @Observable
class KeepAwakePromptState { final class KeepAwakePromptState {
var isPresented = false var isPresented = false
private var hasShownThisSession = false private var hasShownThisSession = false