App/Widgets/LaunchLiveActivity.swift
103 lines · 4372 bytes
1import ActivityKit
2import SwiftUI
3import WidgetKit
4
5struct LaunchLiveActivity: Widget {
6 private let ink = Color(red: 0.95, green: 0.95, blue: 0.93)
7 private let accent = Color(red: 0.88, green: 0.24, blue: 0.19)
8
9 var body: some WidgetConfiguration {
10 ActivityConfiguration(for: LaunchActivityAttributes.self) { context in
11 LaunchActivityView(context: context, ink: ink, accent: accent)
12 .foregroundStyle(ink)
13 } dynamicIsland: { context in
14 DynamicIsland {
15 DynamicIslandExpandedRegion(.leading) {
16 Text(context.attributes.rocket).font(.system(size: 14, weight: .medium))
17 }
18 DynamicIslandExpandedRegion(.trailing) {
19 countdown(context.state.net).font(.system(size: 14, weight: .medium).monospacedDigit())
20 }
21 DynamicIslandExpandedRegion(.bottom) {
22 Text(context.attributes.mission).font(.system(size: 12)).lineLimit(1).opacity(0.8)
23 }
24 } compactLeading: {
25 IslandWidth {
26 Text("T−").font(.system(size: 12, weight: .medium))
27 } limited: {
28 Image(systemName: "arrow.up").foregroundStyle(context.state.status == "Hold" ? accent : ink)
29 }
30 } compactTrailing: {
31 IslandWidth {
32 liveTimer(context.state.net).font(.system(size: 12).monospacedDigit())
33 } limited: {
34 if context.state.status == "Hold" {
35 Text("HOLD").font(.system(size: 12, weight: .medium)).foregroundStyle(accent)
36 } else {
37 liveTimer(context.state.net).font(.system(size: 12).monospacedDigit())
38 }
39 }
40 } minimal: {
41 Image(systemName: "arrow.up")
42 }
43 }
44 .supplementalActivityFamilies([.small])
45 }
46}
47
48private struct LaunchActivityView: View {
49 @Environment(\.activityFamily) private var family
50 @Environment(\.isActivityFullscreen) private var fullscreen
51 var context: ActivityViewContext<LaunchActivityAttributes>
52 var ink: Color
53 var accent: Color
54
55 var body: some View {
56 Group {
57 if family == .small { small } else { medium }
58 }
59 // StandBy: black, to blend with the clock face.
60 .activityBackgroundTint(fullscreen ? .black : Color(red: 0.043, green: 0.078, blue: 0.141))
61 }
62
63 /// Apple Watch Smart Stack and CarPlay.
64 private var small: some View {
65 VStack(alignment: .leading, spacing: 2) {
66 Text(context.attributes.rocket).font(.system(size: 13, weight: .medium)).lineLimit(1)
67 HStack(spacing: 2) {
68 Text(context.state.net > .now ? "T−" : "T+")
69 liveTimer(context.state.net)
70 }
71 .font(.system(size: 17, weight: .medium).monospacedDigit())
72 .foregroundStyle(context.state.status == "Hold" ? accent : ink)
73 }
74 .frame(maxWidth: .infinity, alignment: .leading)
75 .padding(12)
76 }
77
78 private var medium: some View {
79 HStack(alignment: .center, spacing: 12) {
80 VStack(alignment: .leading, spacing: 2) {
81 Text(context.attributes.rocket).font(.system(size: 17, weight: .medium))
82 Text(context.attributes.mission).font(.system(size: 13)).lineLimit(1).opacity(0.8)
83 Text(context.attributes.location).font(.system(size: 11)).lineLimit(1).opacity(0.6)
84 }
85 Spacer()
86 VStack(alignment: .trailing, spacing: 2) {
87 Text(context.state.status.uppercased()).font(.system(size: 10, weight: .medium)).tracking(0.6)
88 .foregroundStyle(context.state.status == "Hold" ? accent : ink)
89 HStack(spacing: 2) {
90 Text(context.state.net > .now ? "T−" : "T+")
91 countdown(context.state.net)
92 }
93 .font(.system(size: 22, weight: .medium).monospacedDigit())
94 }
95 }
96 .padding(16)
97 }
98}
99
100/// Counts down to T-0, then shows elapsed time.
101private func countdown(_ net: Date) -> Text {
102 net > .now ? Text(timerInterval: Date.now...net, countsDown: true) : Text(net, style: .timer)
103}