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/Sources/StoryScreen.swift

v1.0.0
moonrise/App/Sources/StoryScreen.swift history · blame · raw

70 lines · 2863 bytes

 1import Library
 2import SkyRenderer
 3import SwiftUI
 4
 5/// A mission's chapters on the orrery. Moving between chapters runs the clock so the craft travels.
 6struct StoryScreen: View {
 7    var story: MissionStory
 8    @State private var chapter = 0
 9    @State private var date: Date
10    @State private var travel: Task<Void, Never>?
11
12    private let ink = Color(red: 0.95, green: 0.95, blue: 0.93)
13    private let accent = Color(red: 0.88, green: 0.24, blue: 0.19)
14
15    init(story: MissionStory) {
16        self.story = story
17        _date = State(initialValue: story.chapters[0].day)
18    }
19
20    var body: some View {
21        let c = story.chapters[chapter]
22        ScrollView {
23            VStack(alignment: .leading, spacing: 12) {
24                OrreryView(date: date, ink: ink, accent: accent, highlight: story.spacecraft)
25                    .dynamicTypeSize(...DynamicTypeSize.xxLarge)
26                    .accessibilityHidden(true)
27                Text(date.formatted(date: .long, time: .omitted)).font(.subheadline.monospacedDigit()).dimmed(0.7)
28                Text(c.title).font(.title2.weight(.medium))
29                Text(c.text).font(.body).fixedSize(horizontal: false, vertical: true)
30                HStack {
31                    Button { go(to: chapter - 1) } label: { Label("Back", systemImage: "chevron.left") }
32                        .disabled(chapter == 0)
33                    Spacer()
34                    Text("\(chapter + 1) of \(story.chapters.count)").font(.footnote).dimmed(0.6)
35                    Spacer()
36                    Button { go(to: chapter + 1) } label: { Label("Next", systemImage: "chevron.right").labelStyle(.titleAndIcon) }
37                        .disabled(chapter == story.chapters.count - 1)
38                }
39                .font(.subheadline.weight(.medium))
40                .padding(.top, 8)
41                Text("Positions: NASA/JPL Horizons.").font(.caption2).dimmed(0.5).padding(.top, 8)
42            }
43            .padding(20)
44            .readableWidth()
45        }
46        .foregroundStyle(ink)
47        .tint(accent)
48        .background(Color.black.ignoresSafeArea())
49        .navigationTitle(story.spacecraft)
50        .navigationBarTitleDisplayMode(.inline)
51        .onDisappear { travel?.cancel() }
52    }
53
54    /// Moves to chapter `i`, running the date there over about 1.5 s.
55    private func go(to i: Int) {
56        let from = date, to = story.chapters[i].day
57        chapter = i
58        travel?.cancel()
59        travel = Task {
60            let frames = 45
61            for f in 1...frames {
62                try? await Task.sleep(for: .milliseconds(33))
63                if Task.isCancelled { return }
64                let t = Double(f) / Double(frames)
65                let eased = t * t * (3 - 2 * t)
66                date = from.addingTimeInterval(to.timeIntervalSince(from) * eased)
67            }
68        }
69    }
70}