summaryrefslogtreecommitdiff
path: root/DomainDig/SweepActivityController.swift
blob: e6d179ea4ea3174eeb4435f1188a805729766973 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import ActivityKit
import Foundation

/// Starts, updates, and ends the sweep Live Activity around a batch run.
///
/// Holds the activity's `id` (a Sendable `String`) rather than the
/// `Activity` object itself. `Activity` is not Sendable, and sending the
/// stored reference into the fire-and-forget update task while `self` still
/// held it was a Swift 6 region-isolation violation (issue #27). Each task
/// re-resolves the activity via `Activity.activities`, ActivityKit's
/// sanctioned lookup, so nothing non-Sendable crosses an isolation boundary.
@MainActor
final class SweepActivityController {
    static let shared = SweepActivityController()

    private var activityID: String?

    private init() { /* Singleton; use the shared instance. */ }

    func begin(title: String, total: Int) {
        guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
        // A previous activity that never ended (e.g. app killed mid-sweep)
        // would otherwise linger; replace it.
        end(changed: 0, warnings: 0, immediately: true)

        let state = SweepActivityAttributes.ContentState(
            completed: 0,
            total: total,
            currentDomain: nil,
            changed: 0,
            warnings: 0
        )
        let activity = try? Activity.request(
            attributes: SweepActivityAttributes(title: title, startedAt: Date()),
            content: ActivityContent(state: state, staleDate: nil)
        )
        activityID = activity?.id
    }

    func update(completed: Int, total: Int, currentDomain: String?) {
        guard let activityID else { return }
        let state = SweepActivityAttributes.ContentState(
            completed: completed,
            total: total,
            currentDomain: currentDomain,
            changed: 0,
            warnings: 0
        )
        Task {
            guard let activity = Self.activity(withID: activityID) else { return }
            await activity.update(ActivityContent(state: state, staleDate: nil))
        }
    }

    func end(changed: Int, warnings: Int, immediately: Bool = false) {
        guard let activityID else { return }
        self.activityID = nil
        Task {
            guard let activity = Self.activity(withID: activityID) else { return }
            let total = activity.content.state.total
            let state = SweepActivityAttributes.ContentState(
                completed: total,
                total: total,
                currentDomain: nil,
                changed: changed,
                warnings: warnings
            )
            await activity.end(
                ActivityContent(state: state, staleDate: nil),
                dismissalPolicy: immediately ? .immediate : .after(Date().addingTimeInterval(60))
            )
        }
    }

    private nonisolated static func activity(withID id: String) -> Activity<SweepActivityAttributes>? {
        Activity<SweepActivityAttributes>.activities.first { $0.id == id }
    }
}