// // AlarmLiveActivityWidget.swift // TheNoiseClockWidget // // Created by Matt Bruce on 2/2/26. // import AlarmKit import AppIntents import SwiftUI import WidgetKit /// Live Activity widget for alarm countdown and alerting states. /// Uses AlarmKit's AlarmAttributes for automatic countdown management. struct AlarmLiveActivityWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AlarmAttributes.self) { context in // Lock Screen presentation LockScreenAlarmView( attributes: context.attributes, state: context.state ) } dynamicIsland: { context in DynamicIsland { // Expanded regions - shown when long-pressed or alerting DynamicIslandExpandedRegion(.leading) { if let metadata = context.attributes.metadata { AlarmTitleView(metadata: metadata) } else { Text("Alarm") .font(.caption) } } DynamicIslandExpandedRegion(.trailing) { AlarmProgressView(state: context.state) } DynamicIslandExpandedRegion(.bottom) { ExpandedAlarmView( attributes: context.attributes, state: context.state ) } } compactLeading: { // Compact leading - alarm icon during countdown Image(systemName: "alarm.fill") .foregroundStyle(context.attributes.tintColor) } compactTrailing: { // Compact trailing - countdown text CountdownTextView(state: context.state) .font(.caption2.monospacedDigit()) .foregroundStyle(.secondary) } minimal: { // Minimal - just an alarm icon Image(systemName: "alarm.fill") .foregroundStyle(context.attributes.tintColor) } } } } // MARK: - Lock Screen View struct LockScreenAlarmView: View { let attributes: AlarmAttributes let state: AlarmPresentationState private var alarmLabel: String { attributes.metadata?.label ?? "Alarm" } private var alarmId: String { attributes.metadata?.alarmId ?? "" } var body: some View { VStack(spacing: 12) { // Alarm label Text(alarmLabel) .font(.headline) .foregroundStyle(.primary) // Content based on state if case .countdown(let countdown) = state.mode { // Countdown state - show timer VStack(spacing: 4) { Text("Alarm in") .font(.caption) .foregroundStyle(.secondary) Text(timerInterval: Date.now...countdown.fireDate, countsDown: true) .font(.system(size: 48, weight: .bold, design: .rounded)) .monospacedDigit() .foregroundStyle(attributes.tintColor) } } else if case .paused = state.mode { Text("Paused") .font(.title3) .foregroundStyle(.secondary) } else { // Alerting state - show ringing UI with action buttons VStack(spacing: 16) { Image(systemName: "alarm.waves.left.and.right.fill") .font(.system(size: 40)) .foregroundStyle(attributes.tintColor) .symbolEffect(.bounce.byLayer, options: .repeating) Text("Alarm Ringing") .font(.title3.weight(.semibold)) // Action buttons HStack(spacing: 20) { // Snooze button Button(intent: SnoozeAlarmIntent(alarmId: alarmId)) { Label("Snooze", systemImage: "zzz") .font(.callout.weight(.medium)) } .buttonStyle(.bordered) .tint(.blue) // Stop button Button(intent: StopAlarmIntent(alarmId: alarmId)) { Label("Stop", systemImage: "stop.fill") .font(.callout.weight(.medium)) } .buttonStyle(.borderedProminent) .tint(.red) } } } } .padding() .frame(maxWidth: .infinity) } } // MARK: - Expanded Dynamic Island View struct ExpandedAlarmView: View { let attributes: AlarmAttributes let state: AlarmPresentationState private var alarmId: String { attributes.metadata?.alarmId ?? "" } private var isAlerting: Bool { if case .countdown = state.mode { return false } if case .paused = state.mode { return false } return true } var body: some View { if isAlerting { // Alerting state - show action buttons HStack(spacing: 16) { Button(intent: SnoozeAlarmIntent(alarmId: alarmId)) { Text("Snooze") .font(.caption.weight(.medium)) .frame(maxWidth: .infinity) } .buttonStyle(.bordered) .tint(.blue) Button(intent: StopAlarmIntent(alarmId: alarmId)) { Text("Stop") .font(.caption.weight(.medium)) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .tint(.red) } } else { // Countdown state - show countdown info HStack { CountdownTextView(state: state) .font(.headline) Spacer() AlarmProgressView(state: state) .frame(maxHeight: 30) } } } } // MARK: - Countdown Text View struct CountdownTextView: View { let state: AlarmPresentationState var body: some View { if case .countdown(let countdown) = state.mode { Text(timerInterval: Date.now...countdown.fireDate, countsDown: true) .monospacedDigit() .lineLimit(1) } else if case .paused = state.mode { Text("Paused") } else { Text("Now!") .fontWeight(.bold) } } } // MARK: - Progress View struct AlarmProgressView: View { let state: AlarmPresentationState var body: some View { if case .countdown(let countdown) = state.mode { ProgressView( timerInterval: Date.now...countdown.fireDate, label: { EmptyView() }, currentValueLabel: { Text("") } ) .progressViewStyle(.circular) } else if case .paused = state.mode { Image(systemName: "pause.fill") } else { Image(systemName: "alarm.waves.left.and.right.fill") .symbolEffect(.pulse) } } } // MARK: - Title View struct AlarmTitleView: View { let metadata: NoiseClockAlarmMetadata var body: some View { Text(metadata.label) .font(.caption) .lineLimit(1) } }