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/YearSheet.swift

main
moonrise/App/Sources/YearSheet.swift history · blame · raw

135 lines · 5873 bytes

4 symbols in this file
  1import SwiftUI
  2
  3/// Picks a year for the historical sky, from 3000 BCE to 3000 CE.
  4struct YearSheet: View {
  5    @Binding var year: Int?
  6    var south = false
  7    @State private var draft: Int
  8    @Environment(\.dismiss) private var dismiss
  9
 10    static let range = -2999...3000
 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    private let moments: [(String, Int)] = [
 16        ("Great Pyramid built, Thuban at the pole", -2559),
 17        ("Ptolemy's Almagest", 150),
 18        ("Supernova that made the Crab Nebula", 1054),
 19        ("Galileo's telescope", 1610),
 20        ("Apollo 11", 1969),
 21        ("Polaris closest to the pole", 2100),
 22    ]
 23
 24    init(year: Binding<Int?>, south: Bool = false) {
 25        _year = year
 26        self.south = south
 27        _draft = State(initialValue: year.wrappedValue ?? Calendar.current.component(.year, from: .now))
 28    }
 29
 30    static func label(_ y: Int) -> String { y > 0 ? "\(y) CE" : "\(1 - y) BCE" }
 31
 32    private var poleStar: (name: String, degrees: Double)? {
 33        SkyModel.poleStar(at: SkyModel.date(.now, inYear: draft), south: south)
 34    }
 35
 36    var body: some View {
 37        NavigationStack {
 38            ScrollView {
 39                VStack(alignment: .leading, spacing: 0) {
 40                    Text("Sky in another year").font(.title.weight(.medium))
 41                    Rectangle().fill(accent).frame(width: 48, height: 5).padding(.top, 8).padding(.bottom, 12)
 42                    Text("Tonight's date and time in any year from 3000 BCE to 3000 CE. The stars drift with precession and their own motion.")
 43                        .font(.subheadline).foregroundStyle(ink.opacity(0.75))
 44
 45                    yearPicker.padding(.top, 28)
 46
 47                    Text("Moments").font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8)
 48                        .foregroundStyle(ink.opacity(0.6)).padding(.top, 32).padding(.bottom, 4)
 49                    ForEach(moments, id: \.1) { m in moment(m.0, m.1) }
 50
 51                    Text("Dates before 1582 are in the Julian calendar. Planet positions far from today are approximate.")
 52                        .font(.caption2).foregroundStyle(ink.opacity(0.5)).padding(.top, 20)
 53                }
 54                .padding(20)
 55                .readableWidth()
 56            }
 57            .background(Color.black.ignoresSafeArea())
 58            .foregroundStyle(ink)
 59            .tint(accent)
 60            .toolbar {
 61                ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
 62                ToolbarItem(placement: .confirmationAction) {
 63                    Button("Show") {
 64                        year = draft == Calendar.current.component(.year, from: .now) ? nil : draft
 65                        dismiss()
 66                    }
 67                    .fontWeight(.semibold)
 68                }
 69            }
 70        }
 71    }
 72
 73    private var yearPicker: some View {
 74        VStack(spacing: 14) {
 75            VStack(spacing: 2) {
 76                Text(String(draft > 0 ? draft : 1 - draft))
 77                    .font(.system(size: 64, weight: .light).monospacedDigit())
 78                    .contentTransition(.numericText())
 79                Text(draft > 0 ? "CE" : "BCE").font(.subheadline.weight(.medium)).tracking(2).foregroundStyle(ink.opacity(0.7))
 80            }
 81            .frame(maxWidth: .infinity)
 82            .accessibilityElement(children: .combine)
 83            .accessibilityLabel(Self.label(draft))
 84
 85            if let p = poleStar {
 86                Text(String(format: "Pole star: %@, %.1f° from the pole", p.name, p.degrees))
 87                    .font(.footnote).foregroundStyle(ink.opacity(0.75))
 88            }
 89
 90            Slider(value: Binding(get: { Double(draft) }, set: { draft = Int($0.rounded()) }), in: Double(Self.range.lowerBound)...Double(Self.range.upperBound))
 91                .accessibilityLabel("Year")
 92                .accessibilityValue(Self.label(draft))
 93            HStack {
 94                Text("3000 BCE"); Spacer(); Text("1 CE"); Spacer(); Text("3000 CE")
 95            }
 96            .font(.caption2).foregroundStyle(ink.opacity(0.5))
 97            .accessibilityHidden(true)
 98
 99            HStack(spacing: 8) {
100                step("−100", -100); step("−1", -1); step("+1", 1); step("+100", 100)
101            }
102            .padding(.top, 4)
103        }
104        .padding(20)
105        .background(ink.opacity(0.06), in: RoundedRectangle(cornerRadius: 20))
106        .animation(.snappy, value: draft)
107    }
108
109    private func step(_ title: String, _ by: Int) -> some View {
110        Button { draft = min(max(draft + by, Self.range.lowerBound), Self.range.upperBound) } label: {
111            Text(title).font(.subheadline.weight(.medium).monospacedDigit())
112                .frame(maxWidth: .infinity, minHeight: 40)
113                .background(ink.opacity(0.08), in: Capsule())
114        }
115        .buttonStyle(.plain)
116        .accessibilityLabel(by > 0 ? "\(by) years later" : "\(-by) years earlier")
117    }
118
119    private func moment(_ title: String, _ y: Int) -> some View {
120        Button { draft = y } label: {
121            HStack(alignment: .firstTextBaseline, spacing: 14) {
122                Text(Self.label(y)).font(.subheadline.weight(.medium).monospacedDigit())
123                    .foregroundStyle(draft == y ? accent : ink.opacity(0.7))
124                    .frame(width: 84, alignment: .leading)
125                Text(title).font(.subheadline)
126                Spacer(minLength: 0)
127                if draft == y { Image(systemName: "checkmark").font(.footnote.weight(.semibold)).foregroundStyle(accent) }
128            }
129            .padding(.vertical, 12)
130            .overlay(alignment: .bottom) { Rectangle().fill(ink.opacity(0.12)).frame(height: 0.5) }
131            .contentShape(Rectangle())
132        }
133        .buttonStyle(.plain)
134    }
135}