learning_ai_clock/ios/ChronoMindWatch/Complications/WatchComplications.swift
saravanakumardb1 d179c4c624 feat(watch,mac): complete watchOS + macOS companion targets
- watchOS: add WatchSessionManager (WCSession bridge), WatchNotificationHandler
  (snooze/dismiss actions), recommendations() for AppIntentTimelineProvider
- watchOS: WatchTimerStore tick loop with cascade haptics, App Group sync
- macOS: CreateTimerSheet (countdown/alarm/pomodoro), launch-at-login toggle
- macOS: MenuBarState showCreateSheet, MacSettingsView data tab
- Xcode project updated with new file references
2026-03-27 11:28:13 -07:00

247 lines
7.3 KiB
Swift

// Watch Complications
// WidgetKit complications for Apple Watch faces
import SwiftUI
import WidgetKit
// MARK: - Complication Timeline Provider
struct WatchComplicationProvider: AppIntentTimelineProvider {
typealias Entry = WatchComplicationEntry
typealias Intent = WatchComplicationIntent
func placeholder(in context: Context) -> WatchComplicationEntry {
WatchComplicationEntry(
date: Date(),
label: "Standup",
targetTime: Date().addingTimeInterval(3600),
urgency: .important,
hasTimer: true
)
}
func snapshot(for configuration: WatchComplicationIntent, in context: Context) async -> WatchComplicationEntry {
entryFromSharedData()
}
func timeline(for configuration: WatchComplicationIntent, in context: Context) async -> Timeline<WatchComplicationEntry> {
let entry = entryFromSharedData()
let now = Date()
var entries: [WatchComplicationEntry] = []
for minuteOffset in stride(from: 0, through: 30, by: 5) {
let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: now)!
entries.append(WatchComplicationEntry(
date: entryDate,
label: entry.label,
targetTime: entry.targetTime,
urgency: entry.urgency,
hasTimer: entry.hasTimer
))
}
let reloadDate: Date
if entry.hasTimer {
reloadDate = min(entry.targetTime, now.addingTimeInterval(15 * 60))
} else {
reloadDate = now.addingTimeInterval(15 * 60)
}
return Timeline(entries: entries, policy: .after(reloadDate))
}
func recommendations() -> [AppIntentRecommendation<WatchComplicationIntent>] {
[AppIntentRecommendation(intent: WatchComplicationIntent(), description: "Next Timer")]
}
private func entryFromSharedData() -> WatchComplicationEntry {
if let timer = SharedTimerDataManager.shared.readNextFiringTimer() {
return WatchComplicationEntry(
date: Date(),
label: timer.label,
targetTime: timer.targetTime,
urgency: timer.urgency,
hasTimer: true
)
}
return WatchComplicationEntry(
date: Date(),
label: "",
targetTime: Date(),
urgency: .standard,
hasTimer: false
)
}
}
// MARK: - Intent
import AppIntents
struct WatchComplicationIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Timer Complication"
static var description: IntentDescription = "Shows next timer on watch face"
}
// MARK: - Entry
struct WatchComplicationEntry: TimelineEntry {
let date: Date
let label: String
let targetTime: Date
let urgency: UrgencyLevel
let hasTimer: Bool
}
// MARK: - Circular Complication
struct CircularComplicationView: View {
let entry: WatchComplicationEntry
var body: some View {
if entry.hasTimer {
ZStack {
AccessoryWidgetBackground()
VStack(spacing: 0) {
Text(entry.targetTime, style: .timer)
.font(.system(size: 14, weight: .bold, design: .monospaced))
.minimumScaleFactor(0.5)
.widgetAccentable()
}
}
} else {
ZStack {
AccessoryWidgetBackground()
Image(systemName: "clock")
.font(.system(size: 18))
}
}
}
}
// MARK: - Rectangular Complication
struct RectangularComplicationView: View {
let entry: WatchComplicationEntry
var body: some View {
if entry.hasTimer {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(entry.label)
.font(.system(size: 13, weight: .semibold))
.lineLimit(1)
.widgetAccentable()
Text(entry.targetTime, style: .timer)
.font(.system(size: 12, weight: .medium, design: .monospaced))
Text(formatTime(entry.targetTime))
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
Spacer()
}
} else {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("ChronoMind")
.font(.system(size: 13, weight: .semibold))
Text("No timers")
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
Spacer()
}
}
}
}
// MARK: - Inline Complication
struct InlineComplicationView: View {
let entry: WatchComplicationEntry
var body: some View {
if entry.hasTimer {
HStack(spacing: 4) {
Image(systemName: "clock.fill")
Text("\(entry.label) in")
Text(entry.targetTime, style: .timer)
}
} else {
HStack(spacing: 4) {
Image(systemName: "clock")
Text("No timers")
}
}
}
}
// MARK: - Corner Complication
struct CornerComplicationView: View {
let entry: WatchComplicationEntry
var body: some View {
if entry.hasTimer {
Text(entry.targetTime, style: .timer)
.font(.system(size: 12, weight: .bold, design: .monospaced))
.widgetAccentable()
.widgetLabel {
Text(entry.label)
}
} else {
Image(systemName: "clock")
.widgetLabel {
Text("ChronoMind")
}
}
}
}
// MARK: - Family Dispatcher
struct WatchComplicationEntryView: View {
@Environment(\.widgetFamily) var family
let entry: WatchComplicationEntry
var body: some View {
switch family {
case .accessoryCircular:
CircularComplicationView(entry: entry)
case .accessoryRectangular:
RectangularComplicationView(entry: entry)
case .accessoryInline:
InlineComplicationView(entry: entry)
case .accessoryCorner:
CornerComplicationView(entry: entry)
default:
CircularComplicationView(entry: entry)
}
}
}
// MARK: - Widget Definition
struct WatchTimerComplication: Widget {
let kind: String = "WatchTimerComplication"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: WatchComplicationIntent.self,
provider: WatchComplicationProvider()
) { entry in
WatchComplicationEntryView(entry: entry)
.containerBackground(for: .widget) { }
}
.configurationDisplayName("Timer")
.description("Shows next timer countdown on your watch face")
.supportedFamilies([
.accessoryCircular,
.accessoryRectangular,
.accessoryInline,
.accessoryCorner,
])
}
}