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/Watch/MoonriseWatchApp.swift

04b2b1d914788057bcd20945b876847386eb8f65
moonrise/App/Watch/MoonriseWatchApp.swift history · blame · raw

96 lines · 3707 bytes

9 symbols in this file
 1import CoreLocation
 2import SkyEngine
 3import SkyRenderer
 4import SwiftUI
 5
 6@main
 7struct MoonriseWatchApp: App {
 8    var body: some Scene {
 9        WindowGroup { TonightView() }
10    }
11}
12
13/// Moon phase, sunset and sunrise, and the planets up tonight.
14struct TonightView: View {
15    @State private var location = WatchLocation()
16    @State private var tonight: Tonight?
17
18    var body: some View {
19        ScrollView {
20            VStack(alignment: .leading, spacing: 10) {
21                if let t = tonight {
22                    HStack(spacing: 10) {
23                        MoonDisc(fraction: t.moonIllumination, waxing: t.moonPhase < 180)
24                            .frame(width: 44, height: 44)
25                            .accessibilityHidden(true)
26                        VStack(alignment: .leading) {
27                            Text(DailySummary.phaseName(t.moonPhase)).font(.headline)
28                            Text("\(Int((t.moonIllumination * 100).rounded()))% lit").font(.footnote).foregroundStyle(.secondary)
29                        }
30                    }
31                    .accessibilityElement(children: .combine)
32                    if let s = t.sunset { row("Sunset", time(s)) }
33                    if let r = t.sunrise { row("Sunrise", time(r)) }
34                    Text("Planets").font(.footnote.weight(.medium)).foregroundStyle(.secondary).padding(.top, 4)
35                    let up = t.planets.filter { $0.best != nil }
36                    if up.isEmpty { Text("None well placed tonight").font(.footnote) }
37                    ForEach(up, id: \.body) { p in
38                        row(p.body.rawValue.capitalized, p.best.map { "best \(time($0.date))" } ?? "")
39                    }
40                    if !location.located {
41                        Text("Using Chicago until location is allowed.").font(.caption2).foregroundStyle(.secondary)
42                    }
43                } else {
44                    ProgressView()
45                }
46            }
47            .frame(maxWidth: .infinity, alignment: .leading)
48        }
49        .navigationTitle("Tonight")
50        .task(id: location.observer) {
51            let observer = location.observer
52            tonight = await Task.detached { try? Tonight(at: .now, from: observer) }.value
53        }
54        .onAppear { location.start() }
55    }
56
57    private func row(_ label: String, _ value: String) -> some View {
58        HStack {
59            Text(label)
60            Spacer()
61            Text(value).monospacedDigit().foregroundStyle(.secondary)
62        }
63        .font(.footnote)
64    }
65
66    private func time(_ d: Date) -> String { d.formatted(date: .omitted, time: .shortened) }
67}
68
69@MainActor
70@Observable
71final class WatchLocation: NSObject, CLLocationManagerDelegate {
72    private(set) var observer = Observer(latitude: 41.8781, longitude: -87.6298)
73    private(set) var located = false
74    private let manager = CLLocationManager()
75
76    func start() {
77        manager.delegate = self
78        manager.desiredAccuracy = kCLLocationAccuracyKilometer
79        manager.requestWhenInUseAuthorization()
80        manager.requestLocation()
81    }
82
83    nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
84        guard let c = locations.last?.coordinate else { return }
85        Task { @MainActor in
86            observer = Observer(latitude: c.latitude, longitude: c.longitude)
87            located = true
88        }
89    }
90
91    nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {}
92
93    nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
94        Task { @MainActor in self.manager.requestLocation() }
95    }
96}