TheNoiseClock/TheNoiseClockWidget/AlarmLiveActivityWidget.swift

180 lines
5.5 KiB
Swift

//
// AlarmLiveActivityWidget.swift
// TheNoiseClockWidget
//
// Created by Matt Bruce on 2/2/26.
//
import AlarmKit
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<NoiseClockAlarmMetadata>.self) { context in
// Lock Screen presentation
LockScreenAlarmView(
attributes: context.attributes,
state: context.state
)
} dynamicIsland: { context in
DynamicIsland {
// Expanded regions
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 - countdown text
CountdownTextView(state: context.state)
.foregroundStyle(context.attributes.tintColor)
} compactTrailing: {
// Compact trailing - progress ring
AlarmProgressView(state: context.state)
.frame(maxWidth: 32)
} minimal: {
// Minimal - just an alarm icon
Image(systemName: "alarm.fill")
.foregroundStyle(context.attributes.tintColor)
}
}
}
}
// MARK: - Lock Screen View
struct LockScreenAlarmView: View {
let attributes: AlarmAttributes<NoiseClockAlarmMetadata>
let state: AlarmPresentationState
private var alarmLabel: String {
attributes.metadata?.label ?? "Alarm"
}
var body: some View {
VStack(spacing: 12) {
// Alarm label
Text(alarmLabel)
.font(.headline)
.foregroundStyle(.primary)
// Countdown state
if case .countdown(let countdown) = state.mode {
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 {
// Other states (alerting, etc.)
VStack(spacing: 4) {
Image(systemName: "alarm.waves.left.and.right.fill")
.font(.system(size: 32))
.foregroundStyle(attributes.tintColor)
.symbolEffect(.pulse)
Text("Alarm Ringing")
.font(.title3.weight(.semibold))
}
}
}
.padding()
.frame(maxWidth: .infinity)
}
}
// MARK: - Expanded Dynamic Island View
struct ExpandedAlarmView: View {
let attributes: AlarmAttributes<NoiseClockAlarmMetadata>
let state: AlarmPresentationState
var body: some View {
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)
}
}