Created
August 15, 2026 15:58
-
-
Save lanserxt/16dc46607b455bf9e7f4d9a3d72352d2 to your computer and use it in GitHub Desktop.
iOS 27: StateReporting Framework
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| import Observation | |
| import StateReporting | |
| // MARK: - StateReporting Metadata | |
| @available(iOS 27.0, *) | |
| @ReportableMetadata | |
| struct CleanupStableMetadata: Equatable { | |
| /// A low-cardinality value that changes how cleanup behaves. | |
| let mode: String | |
| /// Another low-cardinality configuration value. | |
| let scanStrategy: String | |
| } | |
| @available(iOS 27.0, *) | |
| @ReportableMetadata | |
| struct CleanupVolatileMetadata: Equatable { | |
| /// Changes while the same state is active, so it belongs in volatile metadata. | |
| let progress: Double | |
| let filesFound: Int | |
| let reclaimableMegabytes: Double | |
| } | |
| // MARK: - Model | |
| @available(iOS 27.0, *) | |
| @MainActor | |
| @Observable | |
| final class StorageCleanupModel { | |
| enum Phase: String, CaseIterable { | |
| case idle = "Idle" | |
| case scanning = "Scanning" | |
| case cleaning = "Cleaning" | |
| case completed = "Completed" | |
| case failed = "Failed" | |
| } | |
| private static let reportingDomain = | |
| "com.example.storage-cleaner.cleanup" | |
| /// StateReporter is long-lived and unique for this domain. | |
| private let reporter = StateReporter.reporter( | |
| for: reportingDomain, | |
| stableMetadata: CleanupStableMetadata.self, | |
| volatileMetadata: CleanupVolatileMetadata.self | |
| ) | |
| // UI state | |
| private(set) var phase: Phase = .idle | |
| private(set) var progress = 0.0 | |
| private(set) var filesFound = 0 | |
| private(set) var reclaimableMegabytes = 0.0 | |
| private(set) var isRunning = false | |
| // Stable metadata for this demo run. | |
| let cleanupMode = "Temporary Files" | |
| let scanStrategy = "Safe" | |
| var progressText: String { | |
| progress.formatted( | |
| .percent.precision(.fractionLength(0)) | |
| ) | |
| } | |
| var reclaimableText: String { | |
| let measurement = Measurement( | |
| value: reclaimableMegabytes, | |
| unit: UnitInformationStorage.megabytes | |
| ) | |
| return measurement.formatted( | |
| .measurement( | |
| width: .abbreviated, | |
| usage: .general | |
| ) | |
| ) | |
| } | |
| // MARK: Cleanup Simulation | |
| func startCleanup() async { | |
| guard !isRunning else { | |
| return | |
| } | |
| isRunning = true | |
| filesFound = 0 | |
| reclaimableMegabytes = 0 | |
| progress = 0 | |
| transition(to: .scanning) | |
| // Deliberately sampled at human-interaction timescales. | |
| // StateReporting should not be updated in a tight loop or every frame. | |
| for step in 1...5 { | |
| guard !Task.isCancelled else { | |
| reset() | |
| return | |
| } | |
| try? await Task.sleep(for: .milliseconds(700)) | |
| filesFound += 280 | |
| reclaimableMegabytes += 180 | |
| progress = Double(step) / 5.0 | |
| updateVolatileMetadata() | |
| } | |
| progress = 0 | |
| transition(to: .cleaning) | |
| for step in 1...5 { | |
| guard !Task.isCancelled else { | |
| reset() | |
| return | |
| } | |
| try? await Task.sleep(for: .milliseconds(700)) | |
| progress = Double(step) / 5.0 | |
| updateVolatileMetadata() | |
| } | |
| progress = 1 | |
| transition(to: .completed) | |
| isRunning = false | |
| } | |
| func simulateFailure() { | |
| guard !isRunning else { | |
| return | |
| } | |
| progress = 0 | |
| transition(to: .failed) | |
| } | |
| func reset() { | |
| phase = .idle | |
| progress = 0 | |
| filesFound = 0 | |
| reclaimableMegabytes = 0 | |
| isRunning = false | |
| // nil means that this domain currently has no active state. | |
| reporter.reportTransition(to: nil) | |
| } | |
| // MARK: StateReporting | |
| private var stableMetadata: CleanupStableMetadata { | |
| CleanupStableMetadata( | |
| mode: cleanupMode, | |
| scanStrategy: scanStrategy | |
| ) | |
| } | |
| private var volatileMetadata: CleanupVolatileMetadata { | |
| CleanupVolatileMetadata( | |
| progress: progress, | |
| filesFound: filesFound, | |
| reclaimableMegabytes: reclaimableMegabytes | |
| ) | |
| } | |
| private func transition(to newPhase: Phase) { | |
| phase = newPhase | |
| reporter.reportTransition( | |
| to: newPhase.rawValue, | |
| stableMetadata: stableMetadata, | |
| volatileMetadata: volatileMetadata | |
| ) | |
| } | |
| private func updateVolatileMetadata() { | |
| reporter.reportVolatileMetadataUpdate( | |
| volatileMetadata | |
| ) | |
| } | |
| } | |
| // MARK: - View | |
| @available(iOS 27.0, *) | |
| struct StorageCleanupView: View { | |
| @State private var model = StorageCleanupModel() | |
| var body: some View { | |
| NavigationStack { | |
| List { | |
| stateSection | |
| .listRowSpacing(8) | |
| metadataSection | |
| controlsSection | |
| } | |
| .navigationTitle("Storage Cleanup") | |
| } | |
| } | |
| private var stateSection: some View { | |
| Section("Current State") { | |
| LabeledContent( | |
| "Phase", | |
| value: model.phase.rawValue | |
| ) | |
| if model.phase == .failed { | |
| Label( | |
| "Cleanup failed. No files were removed.", | |
| systemImage: "xmark.octagon.fill" | |
| ) | |
| .foregroundStyle(.red) | |
| } | |
| VStack(alignment: .leading, spacing: 8) { | |
| ProgressView(value: model.progress) | |
| HStack { | |
| Text("Progress") | |
| Spacer() | |
| Text(model.progressText) | |
| .foregroundStyle(.secondary) | |
| } | |
| } | |
| LabeledContent( | |
| "Files Found", | |
| value: model.filesFound.formatted() | |
| ) | |
| LabeledContent( | |
| "Reclaimable", | |
| value: model.reclaimableText | |
| ) | |
| } | |
| } | |
| private var metadataSection: some View { | |
| Section("Stable Metadata") { | |
| LabeledContent( | |
| "Mode", | |
| value: model.cleanupMode | |
| ) | |
| LabeledContent( | |
| "Strategy", | |
| value: model.scanStrategy | |
| ) | |
| } | |
| } | |
| private var controlsSection: some View { | |
| Section { | |
| Button { | |
| Task { | |
| await model.startCleanup() | |
| } | |
| } label: { | |
| Label( | |
| "Run Cleanup", | |
| systemImage: "sparkles" | |
| ) | |
| } | |
| .disabled(model.isRunning) | |
| Button(role: .destructive) { | |
| model.simulateFailure() | |
| } label: { | |
| Label( | |
| "Simulate Failure", | |
| systemImage: "exclamationmark.triangle" | |
| ) | |
| } | |
| .disabled(model.isRunning) | |
| Button { | |
| model.reset() | |
| } label: { | |
| Label( | |
| "Reset", | |
| systemImage: "arrow.counterclockwise" | |
| ) | |
| } | |
| .disabled(model.isRunning) | |
| } | |
| } | |
| } | |
| #Preview { | |
| if #available(iOS 27.0, *) { | |
| StorageCleanupView() | |
| } else { | |
| // Fallback on earlier versions | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment