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