105 lines
3.8 KiB
Swift
105 lines
3.8 KiB
Swift
//
|
|
// TimeWidgetLiveActivity.swift
|
|
// TimeWidget
|
|
//
|
|
// Created by 송예찬 on 6/13/26.
|
|
//
|
|
|
|
import ActivityKit
|
|
import WidgetKit
|
|
import SwiftUI
|
|
|
|
struct TimeWidgetLiveActivity: Widget {
|
|
var body: some WidgetConfiguration {
|
|
ActivityConfiguration(for: TimerAttributes.self) { context in
|
|
// Lock Screen / Notification Banner
|
|
// Text(timerInterval:) is a system primitive that counts down in real time
|
|
// without waking the app — no Timer or polling needed.
|
|
HStack(spacing: 14) {
|
|
Image(systemName: "timer")
|
|
.font(.title2.weight(.semibold))
|
|
.foregroundStyle(.white)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(context.attributes.sessionName)
|
|
.font(.caption)
|
|
.foregroundStyle(.white.opacity(0.75))
|
|
|
|
let safeEnd = max(Date.now, context.state.endDate)
|
|
Text(timerInterval: Date.now...safeEnd, countsDown: true)
|
|
.font(.title.monospacedDigit().bold())
|
|
.foregroundStyle(.white)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 14)
|
|
.activityBackgroundTint(Color(red: 0.07, green: 0.07, blue: 0.18))
|
|
.activitySystemActionForegroundColor(.white)
|
|
|
|
} dynamicIsland: { context in
|
|
DynamicIsland {
|
|
// Expanded — shown when the user long-presses the Dynamic Island.
|
|
DynamicIslandExpandedRegion(.leading) {
|
|
Label(context.attributes.sessionName, systemImage: "timer")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
DynamicIslandExpandedRegion(.trailing) {
|
|
EmptyView()
|
|
}
|
|
DynamicIslandExpandedRegion(.center) {
|
|
let safeEnd = max(Date.now, context.state.endDate)
|
|
Text(timerInterval: Date.now...safeEnd, countsDown: true)
|
|
.font(.title2.monospacedDigit().bold())
|
|
.foregroundStyle(.primary)
|
|
}
|
|
DynamicIslandExpandedRegion(.bottom) {
|
|
Text("Stay focused")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
} compactLeading: {
|
|
Image(systemName: "timer")
|
|
.foregroundStyle(.indigo)
|
|
} compactTrailing: {
|
|
let safeEnd = max(Date.now, context.state.endDate)
|
|
Text(timerInterval: Date.now...safeEnd, countsDown: true)
|
|
.font(.caption.monospacedDigit().bold())
|
|
.foregroundStyle(.primary)
|
|
.frame(width: 52)
|
|
} minimal: {
|
|
Image(systemName: "timer")
|
|
.foregroundStyle(.indigo)
|
|
}
|
|
.keylineTint(.indigo)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
extension TimerAttributes {
|
|
fileprivate static var preview: TimerAttributes {
|
|
TimerAttributes(sessionName: "Deep Work")
|
|
}
|
|
}
|
|
|
|
extension TimerAttributes.ContentState {
|
|
fileprivate static var active: TimerAttributes.ContentState {
|
|
TimerAttributes.ContentState(endDate: Date.now.addingTimeInterval(22 * 60))
|
|
}
|
|
fileprivate static var nearEnd: TimerAttributes.ContentState {
|
|
TimerAttributes.ContentState(endDate: Date.now.addingTimeInterval(2 * 60))
|
|
}
|
|
}
|
|
|
|
#Preview("Notification", as: .content, using: TimerAttributes.preview) {
|
|
TimeWidgetLiveActivity()
|
|
} contentStates: {
|
|
TimerAttributes.ContentState.active
|
|
TimerAttributes.ContentState.nearEnd
|
|
}
|