import Foundation // Catalyst ausdrücklich ausgenommen: dort gilt `os(iOS)`, aber ActivityKit // gibt es nicht — die Typen sind zwar da und jeder Aufruf ist als nicht // verfügbar markiert. #if os(iOS) && !targetEnvironment(macCatalyst) // `@preconcurrency`, weil `Activity` und `ActivityContent` keine // Sendable-Zusage tragen. Beide bleiben hier ohnehin auf dem Hauptakteur — // die Klasse ist darauf festgelegt, und ActivityKit erwartet seine Aufrufe // von dort. Ohne diese Zeile lehnt Swift 6 jeden Aufruf ab. @preconcurrency import ActivityKit /// Zeigt den Download-Fortschritt im Sperrbildschirm. /// /// Getrennt vom Koordinator, weil Live Activities eine Reihe eigener Fallen /// haben — sie können vom Nutzer abgeschaltet sein, ihre Zahl ist begrenzt, /// und eine nicht beendete Activity bleibt stundenlang stehen. Der Koordinator /// meldet nur „das läuft gerade"; was daraus wird, entscheidet sich hier. @MainActor public final class DownloadActivityPresenter { private var activity: Activity? /// Wann zuletzt aktualisiert wurde. /// /// ActivityKit drosselt häufige Aktualisierungen und verwirft sie /// irgendwann kommentarlos. Der Fortschritt kommt mehrmals je Sekunde — /// ungebremst weitergereicht bliebe die Anzeige irgendwann stehen. private var lastUpdate = Date.distantPast private let minimumInterval: TimeInterval = 1.5 public init() {} /// Bringt die Anzeige auf den neuesten Stand. /// /// `nil` beendet sie — das ist der Fall, wenn nichts mehr läuft. /// `async`, nicht `Task { … }` im Inneren: `Activity` und /// `ActivityContent` sind nicht `Sendable`, und eine Aufgabe würde sie über /// die Isolationsgrenze schicken. So bleibt alles auf dem Hauptakteur. public func update(with state: DownloadActivityAttributes.ContentState?) async { guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } guard let state else { await end() return } if let activity { guard Date().timeIntervalSince(lastUpdate) >= minimumInterval else { return } lastUpdate = Date() await activity.update(ActivityContent(state: state, staleDate: nil)) } else { start(with: state) } } private func start(with state: DownloadActivityAttributes.ContentState) { do { activity = try Activity.request( attributes: DownloadActivityAttributes(), content: ActivityContent(state: state, staleDate: nil) ) lastUpdate = Date() } catch { // Scheitert meist, weil die Höchstzahl gleichzeitiger Activities // erreicht ist. Kein Grund, den Download anzuhalten — die Anzeige // ist Beiwerk. activity = nil } } /// Beendet die Anzeige. public func end() async { guard let activity else { return } self.activity = nil // `.immediate`: eine Download-Anzeige, die nach dem letzten Byte noch // eine Viertelstunde im Sperrbildschirm klebt, ist Müll. await activity.end(nil, dismissalPolicy: .immediate) } } #endif