krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

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}