Archived
The watch receives the phone's payload over WatchConnectivity and writes it into its own App Group container through the same SnapshotStore, so there is one wire format and one reader. The agenda, the detail screen and the filter all read that; the complications read what the app last wrote, since an extension cannot run a WCSession of its own. Complication timelines land on event boundaries rather than a schedule, so the next appointment flips in the exact minute it changes without spending refresh budget. CalendarrWatchShared holds what both watch targets need, attached to each the way Shared/ is attached to the app and the widgets. recommendations() is implemented because watchOS requires it — iOS has a default, the watch does not, since the complication gallery has to be populated. Found by building, not by reading. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
4.4 KiB
Swift
114 lines
4.4 KiB
Swift
import SwiftUI
|
|
import WidgetKit
|
|
import CalendarrCore
|
|
|
|
/// Time, title, location. The variant that answers the actual question.
|
|
struct NextEventComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
@Environment(\.widgetFamily) private var family
|
|
|
|
var body: some View {
|
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
|
switch family {
|
|
case .accessoryInline:
|
|
inline(next)
|
|
default:
|
|
rectangular(next)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func rectangular(_ next: SnapshotEvent?) -> some View {
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
if let next {
|
|
Text(next.isAllDay
|
|
? WatchL10n.t("watch.allday", entry.language)
|
|
: WatchComplicationSupport.time(next.start, language: entry.language))
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.widgetAccentable()
|
|
Text(next.title)
|
|
.font(.system(size: 15))
|
|
.lineLimit(1)
|
|
if !next.location.isEmpty {
|
|
Text(next.location)
|
|
.font(.system(size: 11))
|
|
.lineLimit(1)
|
|
}
|
|
} else {
|
|
Image(systemName: "calendar").font(.system(size: 13)).widgetAccentable()
|
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 13))
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
|
|
private func inline(_ next: SnapshotEvent?) -> some View {
|
|
let text: String = {
|
|
guard let next else { return WatchL10n.t("watch.no_events", entry.language) }
|
|
if next.isAllDay { return next.title }
|
|
return "\(WatchComplicationSupport.time(next.start, language: entry.language)) \(next.title)"
|
|
}()
|
|
return Label(text, systemImage: "calendar")
|
|
}
|
|
}
|
|
|
|
/// The next two, so back-to-back appointments are visible at once.
|
|
struct NextTwoComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
|
|
var body: some View {
|
|
let events = WatchComplicationSupport.upcoming(in: entry.snapshot, at: entry.date, limit: 2)
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
if events.isEmpty {
|
|
Image(systemName: "calendar").font(.system(size: 13)).widgetAccentable()
|
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 13))
|
|
} else {
|
|
ForEach(events) { event in
|
|
HStack(spacing: 5) {
|
|
Text(event.isAllDay
|
|
? WatchL10n.t("watch.allday", entry.language)
|
|
: WatchComplicationSupport.time(event.start, language: entry.language))
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.widgetAccentable()
|
|
Text(event.title)
|
|
.font(.system(size: 13))
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
}
|
|
|
|
/// Weekday and date plus the next appointment, so one slot does both jobs.
|
|
struct DatePlusEventComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
|
|
var body: some View {
|
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(dateLine)
|
|
.font(.system(size: 11, weight: .semibold))
|
|
.widgetAccentable()
|
|
if let next {
|
|
Text(next.isAllDay
|
|
? next.title
|
|
: "\(WatchComplicationSupport.time(next.start, language: entry.language)) \(next.title)")
|
|
.font(.system(size: 14))
|
|
.lineLimit(1)
|
|
} else {
|
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 14))
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
|
|
private var dateLine: String {
|
|
let formatter = DateFormatter()
|
|
formatter.locale = WatchL10n.locale(entry.language)
|
|
formatter.setLocalizedDateFormatFromTemplate("EEE d MMMM")
|
|
return formatter.string(from: entry.date).uppercased()
|
|
}
|
|
}
|