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

59d401dca1a327705632d1e2e20f00f807dee828
moonrise/App/Sources/PassFollowButton.swift history · blame · raw

71 lines · 3347 bytes

4 symbols in this file
 1import ActivityKit
 2import SkyEngine
 3import SwiftUI
 4
 5/// Bell on a pass row: puts the pass on the Lock Screen from 10 minutes before rise. Hidden for passes more
 6/// than a day away.
 7struct PassFollowButton: View {
 8    var pass: SatellitePass
 9    @State private var following = false
10    @State private var message: String?
11
12    var body: some View {
13        if pass.activityStart(now: .now) != nil {
14            Button {
15                following ? unfollow() : follow()
16            } label: {
17                Image(systemName: following ? "bell.fill" : "bell")
18                    .font(.subheadline)
19                    .frame(minWidth: 44, minHeight: 44)
20                    .contentShape(Rectangle())
21            }
22            .buttonStyle(.plain)
23            .accessibilityLabel(following ? "Stop following on Lock Screen" : "Follow on Lock Screen")
24            .onAppear { following = Activity<PassActivityAttributes>.activities.contains { matches($0.attributes, pass) } }
25            .alert(message ?? "", isPresented: Binding { message != nil } set: { if !$0 { message = nil } }) {}
26        }
27    }
28
29    private func follow() {
30        guard ActivityAuthorizationInfo().areActivitiesEnabled else {
31            message = "Live Activities are turned off for Moonrise in Settings."
32            return
33        }
34        guard let start = pass.activityStart(now: .now) else { return }
35        let attributes = PassActivityAttributes(name: pass.displayName, rise: pass.rise, peak: pass.peak, set: pass.set,
36                                                riseAzimuth: pass.riseAzimuth, setAzimuth: pass.setAzimuth,
37                                                maxAltitude: pass.maxAltitude, visible: pass.visible)
38        // It cannot end itself without an update, so it goes stale a minute after set.
39        let content = ActivityContent(state: PassActivityAttributes.ContentState(), staleDate: pass.set.addingTimeInterval(60))
40        do {
41            if start > .now {
42                let rise = pass.rise.formatted(date: .omitted, time: .shortened)
43                let alert = AlertConfiguration(
44                    title: "\(pass.displayName) pass in 10 minutes",
45                    body: "Rises \(SkyEngine.compass(pass.riseAzimuth)) at \(rise), highest \(Int(pass.maxAltitude.rounded()))°, sets \(SkyEngine.compass(pass.setAzimuth)).",
46                    sound: .default)
47                _ = try Activity.request(attributes: attributes, content: content, style: .standard, alertConfiguration: alert, start: start)
48            } else {
49                _ = try Activity.request(attributes: attributes, content: content)
50            }
51            following = true
52        } catch {
53            message = "Couldn't add the pass: \(error.localizedDescription)"
54        }
55    }
56
57    private func unfollow() {
58        let pass = pass
59        Task {
60            for a in Activity<PassActivityAttributes>.activities where matches(a.attributes, pass) {
61                await a.end(nil, dismissalPolicy: .immediate)
62            }
63            following = false
64        }
65    }
66}
67
68/// Rise times shift by a 20 s step each time passes are recomputed.
69private func matches(_ attributes: PassActivityAttributes, _ pass: SatellitePass) -> Bool {
70    attributes.name == pass.displayName && abs(attributes.rise.timeIntervalSince(pass.rise)) < 120
71}