| @@ -8,44 +8,9 @@ struct EclipseLiveActivity: Widget { |
| 8 | 8 | |
| 9 | 9 | var body: some WidgetConfiguration { |
| 10 | 10 | ActivityConfiguration(for: EclipseActivityAttributes.self) { context in |
| 11 | | let a = context.attributes |
| 12 | | VStack(alignment: .leading, spacing: 6) { |
| 13 | | Text("\(a.kind) eclipse · \(a.site)").font(.system(size: 15, weight: .medium)) |
| 14 | | HStack(alignment: .firstTextBaseline) { |
| 15 | | if let c2 = a.c2, let c3 = a.c3 { |
| 16 | | VStack(alignment: .leading, spacing: 0) { |
| 17 | | Text(a.kind == "Total" ? "Totality in" : "Annularity in").font(.system(size: 12)).opacity(0.7) |
| 18 | | countdown(to: c2).font(.system(size: 34, weight: .medium).monospacedDigit()) |
| 19 | | } |
| 20 | | Spacer() |
| 21 | | VStack(alignment: .trailing, spacing: 0) { |
| 22 | | Text("Lasts").font(.system(size: 12)).opacity(0.7) |
| 23 | | Text(timerInterval: c2...c3, countsDown: true).font(.system(size: 34, weight: .medium).monospacedDigit()) |
| 24 | | .foregroundStyle(accent).multilineTextAlignment(.trailing) |
| 25 | | } |
| 26 | | } else { |
| 27 | | VStack(alignment: .leading, spacing: 0) { |
| 28 | | Text("Maximum in").font(.system(size: 12)).opacity(0.7) |
| 29 | | countdown(to: a.peak).font(.system(size: 34, weight: .medium).monospacedDigit()) |
| 30 | | } |
| 31 | | } |
| 32 | | } |
| 33 | | ProgressView(timerInterval: a.c1...a.c4, countsDown: false, label: { EmptyView() }, currentValueLabel: { EmptyView() }) |
| 34 | | .tint(ink) |
| 35 | | HStack { |
| 36 | | ForEach(contacts(a), id: \.0) { name, date in |
| 37 | | VStack(alignment: .leading, spacing: 0) { |
| 38 | | Text(name).font(.system(size: 10)).opacity(0.6) |
| 39 | | Text(clock(date, a)).font(.system(size: 11).monospacedDigit()) |
| 40 | | } |
| 41 | | if name != "C4" { Spacer(minLength: 4) } |
| 42 | | } |
| 43 | | } |
| 44 | | Text(a.timeZone == nil ? "Times in UTC" : "Local time at the site").font(.system(size: 10)).opacity(0.6) |
| 45 | | } |
| 46 | | .padding(16) |
| 47 | | .foregroundStyle(ink) |
| 48 | | .activityBackgroundTint(.black) |
| 11 | EclipseActivityView(attributes: context.attributes, ink: ink, accent: accent) |
| 12 | .foregroundStyle(ink) |
| 13 | .activityBackgroundTint(.black) |
| 49 | 14 | } dynamicIsland: { context in |
| 50 | 15 | let a = context.attributes |
| 51 | 16 | let next = a.c2 ?? a.peak |
| @@ -69,26 +34,96 @@ struct EclipseLiveActivity: Widget { |
| 69 | 34 | } |
| 70 | 35 | } |
| 71 | 36 | } compactLeading: { |
| 72 | | Text(a.c2 == nil ? "Max" : "C2").font(.system(size: 12, weight: .medium)) |
| 37 | IslandWidth { |
| 38 | Text(a.c2 == nil ? "Max" : "C2").font(.system(size: 12, weight: .medium)) |
| 39 | } limited: { |
| 40 | Image(systemName: "sun.max") |
| 41 | } |
| 73 | 42 | } compactTrailing: { |
| 74 | 43 | countdown(to: next).font(.system(size: 12).monospacedDigit()).frame(maxWidth: 64) |
| 75 | 44 | } minimal: { |
| 76 | 45 | Image(systemName: "sun.max") |
| 77 | 46 | } |
| 78 | 47 | } |
| 48 | .supplementalActivityFamilies([.small]) |
| 79 | 49 | } |
| 50 | } |
| 51 | |
| 52 | private struct EclipseActivityView: View { |
| 53 | @Environment(\.activityFamily) private var family |
| 54 | var attributes: EclipseActivityAttributes |
| 55 | var ink: Color |
| 56 | var accent: Color |
| 80 | 57 | |
| 81 | | /// Counts down to `date` and then stays at zero. |
| 82 | | private func countdown(to date: Date) -> Text { |
| 83 | | Text(timerInterval: date.addingTimeInterval(-86_400)...date, countsDown: true) |
| 58 | private var a: EclipseActivityAttributes { attributes } |
| 59 | |
| 60 | var body: some View { |
| 61 | if family == .small { small } else { medium } |
| 84 | 62 | } |
| 85 | 63 | |
| 86 | | private func contacts(_ a: EclipseActivityAttributes) -> [(String, Date)] { |
| 87 | | [("C1", a.c1), ("C2", a.c2), ("Max", a.c2 == nil ? a.peak : nil), ("C3", a.c3), ("C4", a.c4)].compactMap { n, d in d.map { (n, $0) } } |
| 64 | /// Apple Watch Smart Stack and CarPlay. |
| 65 | private var small: some View { |
| 66 | VStack(alignment: .leading, spacing: 2) { |
| 67 | Text("\(a.kind) eclipse").font(.system(size: 13, weight: .medium)).lineLimit(1) |
| 68 | HStack(spacing: 4) { |
| 69 | Text(a.c2 == nil ? "Max" : "C2") |
| 70 | countdown(to: a.c2 ?? a.peak) |
| 71 | } |
| 72 | .font(.system(size: 17, weight: .medium).monospacedDigit()) |
| 73 | } |
| 74 | .frame(maxWidth: .infinity, alignment: .leading) |
| 75 | .padding(12) |
| 88 | 76 | } |
| 89 | 77 | |
| 90 | | /// A string: Text(_:format:) would replace the style's time zone with the environment's. |
| 91 | | private func clock(_ date: Date, _ a: EclipseActivityAttributes) -> String { |
| 92 | | date.formatted(Date.FormatStyle(date: .omitted, time: .standard, timeZone: a.timeZone.flatMap(TimeZone.init(identifier:)) ?? .gmt)) |
| 78 | private var medium: some View { |
| 79 | VStack(alignment: .leading, spacing: 6) { |
| 80 | Text("\(a.kind) eclipse · \(a.site)").font(.system(size: 15, weight: .medium)) |
| 81 | HStack(alignment: .firstTextBaseline) { |
| 82 | if let c2 = a.c2, let c3 = a.c3 { |
| 83 | VStack(alignment: .leading, spacing: 0) { |
| 84 | Text(a.kind == "Total" ? "Totality in" : "Annularity in").font(.system(size: 12)).opacity(0.7) |
| 85 | countdown(to: c2).font(.system(size: 34, weight: .medium).monospacedDigit()) |
| 86 | } |
| 87 | Spacer() |
| 88 | VStack(alignment: .trailing, spacing: 0) { |
| 89 | Text("Lasts").font(.system(size: 12)).opacity(0.7) |
| 90 | Text(timerInterval: c2...c3, countsDown: true).font(.system(size: 34, weight: .medium).monospacedDigit()) |
| 91 | .foregroundStyle(accent).multilineTextAlignment(.trailing) |
| 92 | } |
| 93 | } else { |
| 94 | VStack(alignment: .leading, spacing: 0) { |
| 95 | Text("Maximum in").font(.system(size: 12)).opacity(0.7) |
| 96 | countdown(to: a.peak).font(.system(size: 34, weight: .medium).monospacedDigit()) |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | ProgressView(timerInterval: a.c1...a.c4, countsDown: false, label: { EmptyView() }, currentValueLabel: { EmptyView() }) |
| 101 | .tint(ink) |
| 102 | HStack { |
| 103 | ForEach(contacts(a), id: \.0) { name, date in |
| 104 | VStack(alignment: .leading, spacing: 0) { |
| 105 | Text(name).font(.system(size: 10)).opacity(0.6) |
| 106 | Text(clock(date, a)).font(.system(size: 11).monospacedDigit()) |
| 107 | } |
| 108 | if name != "C4" { Spacer(minLength: 4) } |
| 109 | } |
| 110 | } |
| 111 | Text(a.timeZone == nil ? "Times in UTC" : "Local time at the site").font(.system(size: 10)).opacity(0.6) |
| 112 | } |
| 113 | .padding(16) |
| 93 | 114 | } |
| 94 | 115 | } |
| 116 | |
| 117 | /// Counts down to `date` and then stays at zero. |
| 118 | private func countdown(to date: Date) -> Text { |
| 119 | Text(timerInterval: date.addingTimeInterval(-86_400)...date, countsDown: true) |
| 120 | } |
| 121 | |
| 122 | private func contacts(_ a: EclipseActivityAttributes) -> [(String, Date)] { |
| 123 | [("C1", a.c1), ("C2", a.c2), ("Max", a.c2 == nil ? a.peak : nil), ("C3", a.c3), ("C4", a.c4)].compactMap { n, d in d.map { (n, $0) } } |
| 124 | } |
| 125 | |
| 126 | /// A string: Text(_:format:) would replace the style's time zone with the environment's. |
| 127 | private func clock(_ date: Date, _ a: EclipseActivityAttributes) -> String { |
| 128 | date.formatted(Date.FormatStyle(date: .omitted, time: .standard, timeZone: a.timeZone.flatMap(TimeZone.init(identifier:)) ?? .gmt)) |
| 129 | } |