krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v5.0.1: DomainDig/SweepActivityController.swift · raw
1import ActivityKit
2import Foundation
3
4/// Starts, updates, and ends the sweep Live Activity around a batch run.
5///
6/// Holds the activity's `id` (a Sendable `String`) rather than the
7/// `Activity` object itself. `Activity` is not Sendable, and sending the
8/// stored reference into the fire-and-forget update task while `self` still
9/// held it was a Swift 6 region-isolation violation (issue #27). Each task
10/// re-resolves the activity via `Activity.activities`, ActivityKit's
11/// sanctioned lookup, so nothing non-Sendable crosses an isolation boundary.
12@MainActor
13final class SweepActivityController {
14 static let shared = SweepActivityController()
15
16 private var activityID: String?
17
18 private init() { /* Singleton; use the shared instance. */ }
19
20 func begin(title: String, total: Int) {
21 guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
22 // A previous activity that never ended (e.g. app killed mid-sweep)
23 // would otherwise linger; replace it.
24 end(changed: 0, warnings: 0, immediately: true)
25
26 let state = SweepActivityAttributes.ContentState(
27 completed: 0,
28 total: total,
29 currentDomain: nil,
30 changed: 0,
31 warnings: 0
32 )
33 let activity = try? Activity.request(
34 attributes: SweepActivityAttributes(title: title, startedAt: Date()),
35 content: ActivityContent(state: state, staleDate: nil)
36 )
37 activityID = activity?.id
38 }
39
40 func update(completed: Int, total: Int, currentDomain: String?) {
41 guard let activityID else { return }
42 let state = SweepActivityAttributes.ContentState(
43 completed: completed,
44 total: total,
45 currentDomain: currentDomain,
46 changed: 0,
47 warnings: 0
48 )
49 Task {
50 guard let activity = Self.activity(withID: activityID) else { return }
51 await activity.update(ActivityContent(state: state, staleDate: nil))
52 }
53 }
54
55 func end(changed: Int, warnings: Int, immediately: Bool = false) {
56 guard let activityID else { return }
57 self.activityID = nil
58 Task {
59 guard let activity = Self.activity(withID: activityID) else { return }
60 let total = activity.content.state.total
61 let state = SweepActivityAttributes.ContentState(
62 completed: total,
63 total: total,
64 currentDomain: nil,
65 changed: changed,
66 warnings: warnings
67 )
68 await activity.end(
69 ActivityContent(state: state, staleDate: nil),
70 dismissalPolicy: immediately ? .immediate : .after(Date().addingTimeInterval(60))
71 )
72 }
73 }
74
75 private nonisolated static func activity(withID id: String) -> Activity<SweepActivityAttributes>? {
76 Activity<SweepActivityAttributes>.activities.first { $0.id == id }
77 }
78}