From b4b94d54cbed9334a28cdcd70d3a8a0fd06e04ef Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Fri, 17 Jul 2026 08:36:05 -0500 Subject: Implement v4.6.0: sweep Live Activity, share extension, iPad split view, actionable notifications - Sweep Live Activity: SweepActivityAttributes (Shared/), lock-screen + Dynamic Island UI in the widget extension, driven by SweepActivityController wired into the batch pipeline (begin/update/end); NSSupportsLiveActivities in Info.plist. - Share extension (DomainDigShareExtension): accepts a web URL from the share sheet, extracts the host, and hands it to the app via the App Group inbox (DomainDigShareInbox); the app consumes it on activation and inspects it. - iPad layout: RootTabView uses NavigationSplitView in the regular size class and the tab bar in compact. - Actionable notifications: per-domain threadIdentifier grouping, a Re-inspect action, and tap routing into the domain detail via the intent router. - Bump version to 4.6.0 (build 38) across app, widget, and share targets. --- DomainDig/SweepActivityController.swift | 63 +++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 DomainDig/SweepActivityController.swift (limited to 'DomainDig/SweepActivityController.swift') diff --git a/DomainDig/SweepActivityController.swift b/DomainDig/SweepActivityController.swift new file mode 100644 index 0000000..26fcb2d --- /dev/null +++ b/DomainDig/SweepActivityController.swift @@ -0,0 +1,63 @@ +import ActivityKit +import Foundation + +/// Starts, updates, and ends the sweep Live Activity around a batch run. +@MainActor +final class SweepActivityController { + static let shared = SweepActivityController() + + private var activity: Activity? + + private init() {} + + 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 + ) + activity = try? Activity.request( + attributes: SweepActivityAttributes(title: title, startedAt: Date()), + content: ActivityContent(state: state, staleDate: nil) + ) + } + + func update(completed: Int, total: Int, currentDomain: String?) { + guard let activity else { return } + let state = SweepActivityAttributes.ContentState( + completed: completed, + total: total, + currentDomain: currentDomain, + changed: 0, + warnings: 0 + ) + Task { + await activity.update(ActivityContent(state: state, staleDate: nil)) + } + } + + func end(changed: Int, warnings: Int, immediately: Bool = false) { + guard let activity else { return } + self.activity = nil + let state = SweepActivityAttributes.ContentState( + completed: activity.content.state.total, + total: activity.content.state.total, + currentDomain: nil, + changed: changed, + warnings: warnings + ) + Task { + await activity.end( + ActivityContent(state: state, staleDate: nil), + dismissalPolicy: immediately ? .immediate : .after(Date().addingTimeInterval(60)) + ) + } + } +} -- cgit v1.2.3