diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-manager.md similarity index 63% rename from Proposals/0023-progress-reporter.md rename to Proposals/0023-progress-manager.md index c41810f60..dd01c4b28 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-manager.md @@ -28,18 +28,26 @@ - Introduced `ProgressReporter` type and `assign(count:to:)` for alternative use cases, including multi-parent support - Specified Behavior of `ProgressManager` for `Task` cancellation - Redesigned implementation of custom properties to support both holding values of custom property of `self` and of descendants, and multi-parent support - - Introduced `values(of:)` and `total(of:)` methods to dislay and aggregate values of custom properties in a subtree + - Introduced `values(of:)` and `total(of:)` methods to display and aggregate values of custom properties in a subtree - Restructured examples in Proposed Solution to show the use of `Subprogress` and `ProgressReporter` in different cases and enforce use of `subprogress` as parameter label for methods reporting progress and use of `progressReporter` as property name when returning `ProgressReporter` from a library - Expanded Future Directions - Expanded Alternatives Considered - Moving `FormatStyle` to separate future proposal * **v5** Minor Updates: - - Renamed `manager(totalCount:)` method to `start(totalCount)` + - Renamed `manager(totalCount:)` method to `start(totalCount:)` - Changed the return type of `values(of:)` to be an array of non-optional values - Clarified cycle-detection behavior in `assign(count:to:)` at runtime - Added `CustomStringConvertible` and `CustomDebugStringConvertible` conformance to `ProgressManager` and `ProgressReporter` - Expanded Future Directions - Expanded Alternatives Considered +* **v6** Minor Updates: + - Replaced `withProperties` method with `setCounts` + - Removed `ProgressManager.Values` struct + - Made `ProgressManager` conform to `@dynamicMemberLookup` and moved `subscript(dynamicMember:)` methods from `ProgressManager.Values` to `ProgressManager` + - Changed behavior of API so that additional properties are restricted to either `Int`, `Double`, `String?`, `URL?`, `UInt64` or `Duration` types instead of `any Sendable` types + - Added overloads for `subscript(dynamicMember:)` to account for currently-allowed types + - Added requirements to `ProgressManager.Property` protocol to define summarization and termination (deinit) behavior + - Replaced `total(of:)` with overloads for `summary(of:)` to account for all available types and removed `values(of:)` method ## Table of Contents @@ -66,7 +74,7 @@ This proposal aims to introduce a new Progress Reporting API —— `ProgressMan 3. **Error-Resistant Architecture**: One common mistake/footgun when it comes to progress reporting is reusing the [same progress reporting instance](#advantages-of-using-subprogress-as-currency-type). This tends to lead to mistakenly overwriting its expected unit of work after previous caller has set it, or "over completing" / "double finishing" the report after it's been completed. This API prevents this by introducing strong types with different roles. Additionally, it handles progress delegation, accumulation, and nested reporting automatically, eliminating race conditions and progress calculation errors. -4. **Decoupled Progress and Task Control**: This API focuses exclusively on progress reporting, clearly separating it from task control mechanisms like cancellation, which remain the responsibility of Swift's native concurrency primitives for a more coherent programming model. While this API does not assume any control over tasks, it needs to be consistently handling non-completion of progress so it will react to cancellation by completing the progress upon `deinit`. +4. **Decoupled Progress and Task Control**: This API focuses exclusively on progress reporting, clearly separating it from task control mechanisms like cancellation, which remain the responsibility of Swift's native concurrency primitives for a more coherent programming model. While this API does not assume any control over tasks, it needs to consistently handle non-completion of progress so it will react to cancellation by completing the progress upon `deinit`. 5. **Swift Observation Framework Support**: This API leverages the `@Observable` macro to make progress information automatically bindable to UI components, enabling reactive updates with minimal boilerplate code. The Observation framework also provides a way for developers to observe values of `@Observable` APIs via `AsyncSequence`. @@ -115,7 +123,7 @@ public func makeSalad() async { public func chopFruits() async -> Progress {} ``` -We are forced to await the `chopFruits()` call before returning the `Progress` instance. However, the `Progress` instance that is returned from `chopFruits` already has its `completedUnitCount` equal to `totalUnitCount`. Since the `chopSubprogress` would have been completed before being added as a child to its parent `Progress`, it fails to show incremental progress as the code runs to completion within the method. +We are forced to await the `chopFruits()` call before receiving the `Progress` instance. However, the `Progress` instance that is returned from `chopFruits` already has its `completedUnitCount` equal to `totalUnitCount`. Since the `chopSubprogress` would have been completed before being added as a child to its parent `Progress`, it fails to show incremental progress as the code runs to completion within the method. While it may be possible to use the existing `Progress` to report progress in an `async` function to show incremental progress, by passing `Progress` as an argument to the function reporting progress, it is more error-prone, as shown below: @@ -352,30 +360,68 @@ deadlineTracker.assign(count: 1, to: examCountdown.progressReporter) ### Reporting Progress With Type-Safe Custom Properties -You can define additional properties specific to the operations you are reporting progress on with `@dynamicMemberLookup`. For instance, we pre-define additional file-related properties on `ProgressManager` by extending `ProgressManager` for reporting progress on file operations. +You can define additional properties specific to the operations you are reporting progress on with `@dynamicMemberLookup`. -We can declare a custom additional property as follows: - -```swift -struct Filename: ProgressManager.Property { - typealias Value = String +The currently allowed (Value, Summary) pairs for custom properties are: +- (`Int`, `Int`) +- (`UInt64`, `UInt64`) +- (`Double`, `Double`) +- (`String?`, `[String?]`) +- (`URL?`, `[URL?]`) +- (`UInt64`, `[UInt64]`) +- (`Duration`, `Duration`) - static var defaultValue: String { "" } -} +You can declare a custom additional property that has a `String?` `Value` type and `[String?]` `Summary` type as follows: +```swift extension ProgressManager.Properties { var filename: Filename.Type { Filename.self } + + enum Filename: Sendable, ProgressManager.Property { + + typealias Value = String? + + typealias Summary = [String?] + + static var key: String { return "ExampleApp.Filename" } + + static var defaultValue: String? { return nil } + + static var defaultSummary: [String?] { return [] } + + static func reduce(into summary: inout [String?], value: String?) { + summary.append(value) + } + + static func merge(_ summary1: [String?], _ summary2: [String?]) -> [String?] { + summary1 + summary2 + } + + static func finalSummary(_ parentSummary: [String?], _ selfSummary: [String?]) -> [String?] { + parentSummary + selfSummary + } + } } ``` You can report custom properties using `ProgressManager` as follows: ```swift -let manager: ProgressManager = ... -manager.withProperties { properties in - properties.filename = "Capybara.jpg" // using self-defined custom property - properties.totalByteCount = 1000000 // using pre-defined file-related property +let manager: ProgressManager = ProgressManager(totalCount: 2) +manager.complete(count: 1) +manager.filename = "Capybara.jpg" // using self-defined custom property +manager.totalByteCount = 1000000 // using pre-defined file-related property + +await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + +func doSomething(subprogress: consuming Subprogress? = nil) async { + let manager = subprogress?.start(totalCount: 1) + + manager?.filename = "Snail.jpg" // use self-defined custom property in child `ProgressManager` } + +let filenames = manager.summary(of: ProgressManager.Properties.Filename.self) // get Array type summary of filename in subtree since we defined `Summary` to be `[String?]` +print(filenames) // get ["Capybara.jpg", "Snail.jpg"] since we defined `finalSummary` to include filename cumulatively ``` ### Interoperability with Existing `Progress` @@ -449,23 +495,23 @@ overall.addChild(subprogressThree, withPendingUnitCount: 1) ### `ProgressManager` -`ProgressManager` is an `Observable` and `Sendable` class that developers use to report progress. Specifically, an instance of `ProgressManager` can be used to either track progress of a single task, or track progress of a graph of `ProgressManager` instances. +`ProgressManager` is an `Observable` and `Sendable` class, that you can use to report progress. Specifically, an instance of `ProgressManager` can be used to either track progress of a single task, or track progress of a graph of `ProgressManager` instances. Additionally, `ProgressManager` also uses the `@dynamicMemberLookup` attribute to access or mutate, at runtime, custom `ProgressManager.Property` types that you may declare to track additional properties that add context to progress being reported. ```swift /// An object that conveys ongoing progress to the user for a specified task. -@available(FoundationPreview 6.2, *) +@available(FoundationPreview 6.4, *) +@dynamicMemberLookup @Observable public final class ProgressManager : Sendable, Hashable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { /// The total units of work. public var totalCount: Int? { get } /// The completed units of work. - /// If `self` is indeterminate, the value will be 0. public var completedCount: Int { get } /// The proportion of work completed. /// This takes into account the fraction completed in its children instances if children are present. - /// If `self` is indeterminate, the value will be 0. + /// If `self` is indeterminate, the value will be 0.0. public var fractionCompleted: Double { get } /// The state of initialization of `totalCount`. @@ -485,28 +531,6 @@ overall.addChild(subprogressThree, withPendingUnitCount: 1) /// A debug description. public var debugDescription: String { get } - /// A type that conveys additional task-specific information on progress. - public protocol Property { - - associatedtype Value : Sendable - - /// The default value to return when property is not set to a specific value. - static var defaultValue: Value { get } - } - - /// A container that holds values for properties that convey information about progress. - @dynamicMemberLookup public struct Values : Sendable { - - /// The total units of work. - public var totalCount: Int? { mutating get set } - - /// The completed units of work. - public var completedCount: Int { mutating get set } - - /// Returns a property value that a key path indicates. If value is not defined, returns property's `defaultValue`. - public subscript(dynamicMember key: KeyPath) -> Property.Value { get set } - } - /// Initializes `self` with `totalCount`. /// /// If `totalCount` is set to `nil`, `self` is indeterminate. @@ -527,32 +551,158 @@ overall.addChild(subprogressThree, withPendingUnitCount: 1) /// If a cycle is detected, this will cause a crash at runtime. /// /// - Parameters: - /// - output: A `ProgressReporter` instance. - /// - count: The portion of `totalCount` to be delegated to the `ProgressReporter`. + /// - count: Number of units delegated from `self`'s `totalCount` to `reporter`. + /// - reporter: A `ProgressReporter` instance. public func assign(count: Int, to reporter: ProgressReporter) /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) + + /// Atomically modifies `completedCount` and `totalCount` together to ensure consistency. + /// - Parameter counts: A closure that receives inout references to both count values. + public func setCounts(_ counts: (_ completed: inout Int, _ total: inout Int?) -> Void) + + /// Gets or sets custom integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Int`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom integer property type. + public subscript(dynamicMember key: KeyPath) -> Int where P.Value == Int, P.Summary == Int { get set } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `UInt64`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == UInt64 { get set } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `UInt64` and the summary type is `[UInt64]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == [UInt64] { get set } + + /// Gets or sets custom duration properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Duration`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom duration property type. + public subscript(dynamicMember key: KeyPath) -> Duration where P.Value == Duration, P.Summary == Duration { get set } + + /// Gets or sets custom double properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Double`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom double property type. + public subscript(dynamicMember key: KeyPath) -> Double where P.Value == Double, P.Summary == Double { get set } - /// Accesses or mutates any properties that convey additional information about progress. - public func withProperties( - _ closure: (inout sending Values) throws(E) -> sending T - ) throws(E) -> sending T + /// Gets or sets custom string properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `String?` and the summary type is `[String?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom string property type. + public subscript(dynamicMember key: KeyPath) -> String? where P.Value == String?, P.Summary == [String?] { get set } - /// Returns an array of values for specified property in subtree. - /// - /// - Parameter property: Type of property. - /// - Returns: Array of values for property. - public func values(of property: P.Type) -> [P.Value] + /// Gets or sets custom URL properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `URL?` and the summary type is `[URL?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom URL property type. + public subscript(dynamicMember key: KeyPath) -> URL? where P.Value == URL?, P.Summary == [URL?] { get set } +} +``` - /// Returns the aggregated result of values where type of property is `AdditiveArithmetic`. - /// All values are added together. - /// - /// - Parameters: - /// - property: Type of property. - /// - values: Sum of values. - public func total(of property: P.Type) -> P.Value where P.Value : AdditiveArithmetic +`ProgressManager` also contains methods that summarize additional properties of across a subtree rooted by the `ProgressManager` they are called from. + +```swift +@available(FoundationPreview 6.4, *) +extension ProgressManager { + + /// Returns a summary for a custom integer property across the progress subtree. + /// + /// This method aggregates the values of a custom integer property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the integer property to summarize. Must be a property + /// where both the value and summary types are `Int`. + /// - Returns: An `Int` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == Int, P.Summary == Int + + /// Returns a summary for a custom unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom integer property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the integer property to summarize. Must be a property + /// where both the value and summary types are `UInt64`. + /// - Returns: An `UInt64` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == UInt64, P.Summary == UInt64 + + /// Returns a summary for a custom unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from this progress manager + /// and all its children, returning a consolidated summary value as an array of unsigned integer values. + /// + /// - Parameter property: The type of the unsigned integer property to summarize. Must be a property + /// where the value type is `UInt64` and the summary type is `[UInt64]`. + /// - Returns: A `[UInt64]` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == UInt64, P.Summary == [UInt64] + + /// Returns a summary for a custom duration property across the progress subtree. + /// + /// This method aggregates the values of a custom duration property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the duration property to summarize. Must be a property + /// where both the value and summary types are `Duration`. + /// - Returns: An `Duration` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == Duration, P.Summary == Duration + + /// Returns a summary for a custom double property across the progress subtree. + /// + /// This method aggregates the values of a custom double property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the double property to summarize. Must be a property + /// where both the value and summary types are `Double`. + /// - Returns: A `Double` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == Double, P.Summary == Double + + /// Returns a summary for a custom string property across the progress subtree. + /// + /// This method aggregates the values of a custom string property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the string property to summarize. Must be a property + /// where both the value type is `String?` and the summary type is `[String?]`. + /// - Returns: A `[String?]` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == String?, P.Summary == [String?] + + /// Returns a summary for a custom URL property across the progress subtree. + /// + /// This method aggregates the values of a custom URL property from this progress manager + /// and all its children, returning a consolidated summary value as an array of URLs. + /// + /// - Parameter property: The type of the URL property to summarize. Must be a property + /// where the value type is `URL?` and the summary type is `[URL?]`. + /// - Returns: A `[URL?]` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == URL?, P.Summary == [URL?] } ``` @@ -563,11 +713,11 @@ You call `ProgressManager`'s `subprogress(assigningCount:)` to create a `Subprog The callee will consume `Subprogress` and get the `ProgressManager` by calling `start(totalCount:)`. That `ProgressManager` is used for the function's own progress updates. ```swift -@available(FoundationPreview 6.2, *) /// Subprogress is used to establish parent-child relationship between two instances of `ProgressManager`. /// /// Subprogress is returned from a call to `subprogress(assigningCount:)` by a parent ProgressManager. /// A child ProgressManager is then returned by calling `start(totalCount:)` on a Subprogress. +@available(FoundationPreview 6.4, *) public struct Subprogress: ~Copyable, Sendable { /// Instantiates a ProgressManager which is a child to the parent ProgressManager from which the Subprogress is created. @@ -578,63 +728,108 @@ public struct Subprogress: ~Copyable, Sendable { } ``` -### `ProgressReporter` +### `ProgressManager.Property` -```swift -@available(FoundationPreview 6.2, *) -/// ProgressReporter is used to observe progress updates from a `ProgressManager`. It may also be used to incorporate those updates into another `ProgressManager`. -/// -/// It is read-only and can be added as a child of another ProgressManager. -@Observable public final class ProgressReporter : Sendable, CustomStringConvertible, CustomDebugStringConvertible { +`ProgressManager` contains a protocol `Property` that outlines the requirements for declaring a custom additional property. The currently allowed (Value, Summary) pairs are as follows: +- (`Int`, `Int`) +- (`UInt64`, `UInt64`) +- (`Double`, `Double`) +- (`String?`, `[String?]`) +- (`URL?`, `[URL?]`) +- (`UInt64`, `[UInt64]`) +- (`Duration`, `Duration`) - /// The total units of work. - public var totalCount: Int? { get } +This list of allowed (Value, Summary) pairs may be expanded in the future. Based on pre-existing use cases of additional properties on `Progress`, we believe that the currently allowed (Value, Summary) pairs should suffice for most use cases. - /// The completed units of work. - /// If `self` is indeterminate, the value will be 0. - public var completedCount: Int { get } - - /// The proportion of work completed. - /// This takes into account the fraction completed in its children instances if children are present. - /// If `self` is indeterminate, the value will be 0. - public var fractionCompleted: Double { get } - - /// The state of initialization of `totalCount`. - /// If `totalCount` is `nil`, the value will be `true`. - public var isIndeterminate: Bool { get } - - /// The state of completion of work. - /// If `completedCount` >= `totalCount`, the value will be `true`. - public var isFinished: Bool { get } - - /// A description. - public var description: String { get } - - /// A debug description. - public var debugDescription: String { get } - - /// Reads properties that convey additional information about progress. - public func withProperties( - _ closure: (sending ProgressManager.Values) throws(E) -> sending T - ) throws(E) -> T - - /// Returns an array of values for specified additional property in subtree. - /// The specified property refers to a declared type representing additional progress-related properties - /// that conform to the `ProgressManager.Property` protocol. - /// - /// - Parameter property: Type of property. - /// - Returns: Array of values for property. - public func values(of property: P.Type) -> [P.Value] +```swift +@available(FoundationPreview 6.4, *) +extension ProgressManager { - /// Returns the aggregated result of values for specified `AdditiveArithmetic` property in subtree. - /// The specified property refers to a declared type representing additional progress-related properties - /// that conform to the `ProgressManager.Property` protocol. - /// The specified property also has to be an `AdditiveArithmetic`. For non-`AdditiveArithmetic` types, you should - /// write your own method to aggregate values. + /// A type that conveys additional task-specific information on progress. /// - /// - Parameters property: Type of property. - /// - Returns: Aggregated result of values for property. - public func total(of property: P.Type) -> P.Value where P.Value : AdditiveArithmetic + /// The `Property` protocol defines custom properties that can be associated with progress tracking. + /// These properties allow you to store and aggregate additional information alongside the + /// standard progress metrics such as `totalCount` and `completedCount`. + public protocol Property: SendableMetatype { + + /// The type used for individual values of this property. + /// + /// This associated type represents the type of property values + /// that can be set on progress managers. Must be `Sendable` and `Equatable`. + /// The currently allowed types are `Int`, `Double`, `String?`, `URL?` or `UInt64`. + associatedtype Value: Sendable, Equatable + + /// The type used for aggregated summaries of this property. + /// + /// This associated type represents the type used when summarizing property values + /// across multiple progress managers in a subtree. + /// The currently allowed types are `Int`, `Double`, `[String?]`, `[URL?]` or `[UInt64]`. + associatedtype Summary: Sendable, Equatable + + /// A unique identifier for this property type. + /// + /// The key should use reverse DNS style notation to ensure uniqueness across different + /// frameworks and applications. + /// + /// - Returns: A unique string identifier for this property type. + static var key: String { get } + + /// The default value to return when property is not set to a specific value. + /// + /// This value is used when a progress manager doesn't have an explicit value set + /// for this property type. + /// + /// - Returns: The default value for this property type. + static var defaultValue: Value { get } + + /// The default summary value for this property type. + /// + /// This value is used as the initial summary when no property values have been + /// aggregated yet. + /// + /// - Returns: The default summary value for this property type. + static var defaultSummary: Summary { get } + + /// Reduces a property value into an accumulating summary. + /// + /// This method is called to incorporate individual property values into a summary + /// that represents the aggregated state across multiple progress managers. + /// + /// - Parameters: + /// - summary: The accumulating summary value to modify. + /// - value: The individual property value to incorporate into the summary. + static func reduce(into summary: inout Summary, value: Value) + + /// Merges two summary values into a single combined summary. + /// + /// This method is called to combine summary values from different branches + /// of the progress manager hierarchy into a unified summary. + /// + /// - Parameters: + /// - summary1: The first summary to merge. + /// - summary2: The second summary to merge. + /// - Returns: A new summary that represents the combination of both input summaries. + static func merge(_ summary1: Summary, _ summary2: Summary) -> Summary + + /// Determines how to handle summary data when a progress manager is deinitialized. + /// + /// This method is used when a progress manager in the hierarchy is being + /// deinitialized and its accumulated summary needs to be processed in relation to + /// its parent's summary. The behavior can vary depending on the property type: + /// + /// - For additive properties (like file counts, byte counts): The self summary + /// is typically added to the parent summary to preserve the accumulated progress. + /// - For max-based properties (like estimated time remaining): The parent summary + /// is typically preserved as it represents an existing estimate. + /// - For collection-based properties (like file URLs): The self summary may be + /// discarded to avoid accumulating stale references. + /// + /// - Parameters: + /// - parentSummary: The current summary value of the parent progress manager. + /// - selfSummary: The final summary value from the progress manager being deinitialized. + /// - Returns: The updated summary that replaces the parent's current summary. + static func finalSummary(_ parentSummary: Summary, _ selfSummary: Summary) -> Summary + } } ``` @@ -644,79 +839,330 @@ public struct Subprogress: ~Copyable, Sendable { We pre-declare some of these additional properties that are commonly desired in use cases of progress reporting, including and not limited to, `totalFileCount` and `totalByteCount`. -If you would like to report additional metadata or properties that are not part of the pre-declared additional properties, you can declare additional properties into `ProgressManager.Properties`, similar to how the pre-declared additional properties are declared. - -Additionally, the additional metadata or properties of each `ProgressManager` can be read by calling the `values(of:)` method defined in `ProgressManager`. The `values(of:)` method returns an array of values for each specified property in a subtree. If you would like to get an aggregated value of a property that is an `AdditiveArithmetic` type, you can call the `total(of:)` method defined in `ProgressManager`. +If you would like to report additional metadata or properties that are not part of the pre-declared additional properties, you can declare additional properties into `ProgressManager.Properties`, similar to how the pre-declared additional properties are declared. These additional properties can only have `Value` - `Summary` pairs that are either `Int` - `Int`, `Double` - `Double`, `String?` - `[String?]`, `URL?` - `[URL?]`, or `UInt64` - `[UInt64]`. ```swift -@available(FoundationPreview 6.2, *) +@available(FoundationPreview 6.4, *) extension ProgressManager { - public struct Properties { + @frozen + public enum Properties { /// The total number of files. public var totalFileCount: TotalFileCount.Type { get } - public struct TotalFileCount : Property { + @frozen + public enum TotalFileCount : Sendable, Property { public typealias Value = Int + public typealias Summary = Int + + public static var key: String { get } + public static var defaultValue: Int { get } + + public static var defaultSummary: Int { get } + + public static func reduce(into summary: inout Int, value: Int) + + public static func merge(_ summary1: Int, _ summary2: Int) -> Int + + public static func finalSummary(_ parentSummary: Int, _ selfSummary: Int) -> Int } /// The number of completed files. public var completedFileCount: CompletedFileCount.Type { get } - - public struct CompletedFileCount : Property { + + @frozen + public enum CompletedFileCount : Sendable, Property { public typealias Value = Int + public typealias Summary = Int + + public static var key: String { get } + public static var defaultValue: Int { get } + + public static var defaultSummary: Int { get } + + public static func reduce(into summary: inout Int, value: Int) + + public static func merge(_ summary1: Int, _ summary2: Int) -> Int + + public static func finalSummary(_ parentSummary: Int, _ selfSummary: Int) -> Int } /// The total number of bytes. public var totalByteCount: TotalByteCount.Type { get } - public struct TotalByteCount : Property { + @frozen + public enum TotalByteCount : Sendable, Property { public typealias Value = UInt64 + public typealias Summary = UInt64 + + public static var key: String { get } + public static var defaultValue: UInt64 { get } + + public static var defaultSummary: UInt64 { get } + + public static func reduce(into summary: inout UInt64, value: UInt64) + + public static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 + + public static func finalSummary(_ parentSummary: UInt64, _ selfSummary: UInt64) -> UInt64 } /// The number of completed bytes. public var completedByteCount: CompletedByteCount.Type { get } - public struct CompletedByteCount : Property { + @frozen + public enum CompletedByteCount : Sendable, Property { public typealias Value = UInt64 + public typealias Summary = UInt64 + + public static var key: String { get } + public static var defaultValue: UInt64 { get } + + public static var defaultSummary: UInt64 { get } + + public static func reduce(into summary: inout UInt64, value: UInt64) + + public static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 + + public static func finalSummary(_ parentSummary: UInt64, _ selfSummary: UInt64) -> UInt64 } /// The throughput, in bytes per second. public var throughput: Throughput.Type { get } - public struct Throughput : Property { + @frozen + public enum Throughput : Sendable, Property { public typealias Value = UInt64 + public typealias Summary = [UInt64] + + public static var key: String { get } + public static var defaultValue: UInt64 { get } + + public static var defaultSummary: [UInt64] { get } + + public static func reduce(into summary: inout [UInt64], value: UInt64) + + public static func merge(_ summary1: [UInt64], _ summary2: [UInt64]) -> [UInt64] + + public static func finalSummary(_ parentSummary: [UInt64], _ selfSummary: [UInt64]) -> [UInt64] } - /// The amount of time remaining in the processing of files. + /// The amount of time remaining in operation. public var estimatedTimeRemaining: EstimatedTimeRemaining.Type { get } - public struct EstimatedTimeRemaining : Property { + @frozen + public enum EstimatedTimeRemaining : Sendable, Property { public typealias Value = Duration + public typealias Summary = Duration + + public static var key: String { get } + public static var defaultValue: Duration { get } + + public static var defaultSummary: Duration { get } + + public static func reduce(into summary: inout Duration, value: Duration) + + public static func merge(_ summary1: Duration, _ summary2: Duration) -> Duration + + public static func finalSummary(_ parentSummary: Duration, _ selfSummary: Duration) -> Duration } } } ``` +### `ProgressReporter` + +`ProgressReporter` is a read-only instance of its underlying `ProgressManager`. It is also used as an adapter to add a `ProgressManager` as a child to more than one parent `ProgressManager` by calling the `assign(count:to:)` method on a parent `ProgressManager`. + +```swift +/// ProgressReporter is used to observe progress updates from a `ProgressManager`. It may also be used to incorporate those updates into another `ProgressManager`. +/// +/// It is read-only and can be added as a child of another ProgressManager. +@available(FoundationPreview 6.4, *) +@dynamicMemberLookup +@Observable public final class ProgressReporter : Sendable, Hashable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { + + public typealias Property = ProgressManager.Property + + /// The total units of work. + public var totalCount: Int? { get } + + /// The completed units of work. + public var completedCount: Int { get } + + /// The proportion of work completed. + /// This takes into account the fraction completed in its children instances if children are present. + /// If `self` is indeterminate, the value will be 0. + public var fractionCompleted: Double { get } + + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. + public var isIndeterminate: Bool { get } + + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. + public var isFinished: Bool { get } + + /// A description. + public var description: String { get } + + /// A debug description. + public var debugDescription: String { get } + + /// Gets custom integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Int`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom integer property type. + public subscript(dynamicMember key: KeyPath) -> Int { get } + + /// Gets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `UInt64`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 { get } + + /// Gets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `UInt64` and the summary type is `[UInt64]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 { get } + + /// Gets custom duration properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `Duration` and the summary type is `Duration`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom duration property type. + public subscript(dynamicMember key: KeyPath) -> Duration { get } + + /// Gets custom double properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Double`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom double property type. + public subscript(dynamicMember key: KeyPath) -> Double { get } + + /// Gets custom string properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `String?` and the summary type is `[String?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom string property type. + public subscript(dynamicMember key: KeyPath) -> String? { get } + + /// Gets custom URL properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `URL?` and the summary type is `[URL?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom URL property type. + public subscript(dynamicMember key: KeyPath) -> URL? { get } + + /// Returns a summary for a custom integer property across the progress subtree. + /// + /// This method aggregates the values of a custom integer property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the integer property to summarize. Must be a property + /// where both the value and summary types are `Int`. + /// - Returns: An `Int` summary value for the specified property. + public func summary(of property: P.Type) -> Int where P.Value == Int, P.Summary == Int + + /// Returns a summary for a custom unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the unsigned integer property to summarize. Must be a property + /// where both the value and summary types are `UInt64`. + /// - Returns: An `UInt64` summary value for the specified property. + public func summary(of property: P.Type) -> UInt64 where P.Value == UInt64, P.Summary == UInt64 + + /// Returns a summary for a custom unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from the underlying progress manager + /// and all its children, returning a consolidated summary value as an array of unsigned integer values. + /// + /// - Parameter property: The type of the unsigned integer property to summarize. Must be a property + /// where the value type is `UInt64` and the summary type is `[UInt64]`. + /// - Returns: A `[UInt64]` summary value for the specified property. + public func summary(of property: P.Type) -> [UInt64] where P.Value == UInt64, P.Summary == [UInt64] + + /// Returns a summary for a custom duration property across the progress subtree. + /// + /// This method aggregates the values of a custom duration property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the duration property to summarize. Must be a property + /// where both the value and summary types are `Duration`. + /// - Returns: An `Duraton` summary value for the specified property. + public func summary(of property: P.Type) -> Duration where P.Value == Duration, P.Summary == Duration + + /// Returns a summary for a custom double property across the progress subtree. + /// + /// This method aggregates the values of a custom double property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the double property to summarize. Must be a property + /// where both the value and summary types are `Double`. + /// - Returns: A `Double` summary value for the specified property. + public func summary(of property: P.Type) -> Double where P.Value == Double, P.Summary == Double + + /// Returns a summary for a custom string property across the progress subtree. + /// + /// This method aggregates the values of a custom string property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the string property to summarize. Must be a property + /// where both the value type is `String?` and the summary type is `[String?]`. + /// - Returns: A `[String?]` summary value for the specified property. + public func summary(of property: P.Type) -> [String?] where P.Value == String?, P.Summary == [String?] + + /// Returns a summary for a custom URL property across the progress subtree. + /// + /// This method aggregates the values of a custom URL property from the underlying progress manager + /// and all its children, returning a consolidated summary value as an array of URLs. + /// + /// - Parameter property: The type of the URL property to summarize. Must be a property + /// where the value type is `URL?` and the summary type is `[URL?]`. + /// - Returns: A `[URL?]` summary value for the specified property. + public func summary(of property: P.Type) -> [URL?] where P.Value == URL?, P.Summary == [URL?] +} +``` + ### Cancellation in `ProgressManager` While this API does not assume any control over tasks, it needs to react to a task being cancelled, or a `Subprogress` not being consumed. @@ -748,12 +1194,12 @@ To allow frameworks which may have dependencies on the pre-existing progress-rep #### `ProgressManager` (Parent) - `Foundation.Progress` (Child) -To add an instance of `Foundation.Progress` as a child to an instance of `ProgressManager`, we pass an `Int` for the portion of `ProgressManager`'s `totalCount` `Foundation.Progress` should take up and a `Foundation.Progress` instance to `assign(count: to:)`. The `ProgressManager` instance will track the `Foundation.Progress` instance just like any of its `ProgressManager` children. +To add an instance of `Foundation.Progress` as a child to an instance of `ProgressManager`, we pass an `Int` for the portion of `ProgressManager`'s `totalCount` `Foundation.Progress` should take up and a `Foundation.Progress` instance to `assign(count:to:)`. The `ProgressManager` instance will track the `Foundation.Progress` instance just like any of its `ProgressManager` children. >The choice of naming the interop method as `subprogress(assigningCount: to:)` is to keep the syntax consistent with the method used to add a `ProgressManager` instance to the progress tree using this new API, `subprogress(assigningCount:)`. ```swift -@available(FoundationPreview 6.2, *) +@available(FoundationPreview 6.4, *) extension ProgressManager { /// Adds a Foundation's `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. /// @@ -766,12 +1212,12 @@ extension ProgressManager { #### `Foundation.Progress` (Parent) - `ProgressManager` (Child) -To add an instance of `ProgressManager` as a child to an instance of the existing `Foundation.Progress`, the `Foundation.Progress` instance calls `makeChild(count:)` to get a `Subprogress` instance that can be passed as a parameter to a function that reports progress. The `Foundation.Progress` instance will track the `ProgressManager` instance as a child, just like any of its `Progress` children. +To add an instance of `ProgressManager` as a child to an instance of the existing `Foundation.Progress`, the `Foundation.Progress` instance calls `makeChild(withPendingUnitCount:)` to get a `Subprogress` instance that can be passed as a parameter to a function that reports progress. The `Foundation.Progress` instance will track the `ProgressManager` instance as a child, just like any of its `Progress` children. >The choice of naming the interop methods as `makeChild(withPendingUnitCount:)` and `addChild(_:withPendingUnitCount` is to keep the syntax consistent with the method used to add a `Foundation.Progress` instance as a child to another `Foundation.Progress`. ```swift -@available(FoundationPreview 6.2, *) +@available(FoundationPreview 6.4, *) extension Progress { /// Returns a Subprogress which can be passed to any method that reports progress /// and can be initialized into a child `ProgressManager` to the `self`. @@ -787,7 +1233,7 @@ extension Progress { /// Adds a ProgressReporter as a child to a Foundation.Progress. /// /// - Parameters: - /// - output: A `ProgressReporter` instance. + /// - reporter: A `ProgressReporter` instance. /// - count: Number of units delegated from `self`'s `totalCount` to Progress Reporter. public func addChild(_ reporter: ProgressReporter, withPendingUnitCount count: Int) } @@ -867,7 +1313,7 @@ func g() async { // App code func f() async { - var progressManager = ProgressManager(totalUnitCount: 1) + var progressManager = ProgressManager(totalCount: 1) await g() // progress consumed } @@ -894,7 +1340,7 @@ func f() async { Additionally, progress reporting being directly integrated into the structured concurrency model would also introduce a non-trivial trade-off. Supporting multi-parent use cases, or the ability to construct an acyclic graph for progress is a heavily-desired feature for this API, but structured concurrency, which assumes a tree structure, would inevitably break this use case. ### Add Convenience Method to Existing `Progress` for Easier Instantiation of Child Progress -While the explicit model has concurrency support via completion handlers, the usage pattern does not fit well with async/await, because which an instance of `Progress` returned by an asynchronous function would return after code is executed to completion. In the explicit model, to add a child to a parent progress, we pass an instantiated child progress object into the `addChild(child:withPendingUnitCount:)` method. In this alternative, we add a convenience method that bears the function signature `makeChild(pendingUnitCount:)` to the `Progress` class. This method instantiates an empty progress and adds itself as a child, allowing developers to add a child progress to a parent progress without having to instantiate a child progress themselves. The additional method reads as follows: +While the explicit model has concurrency support via completion handlers, the usage pattern does not fit well with async/await, because an instance of `Progress` returned by an asynchronous function would return after code is executed to completion. In the explicit model, to add a child to a parent progress, we pass an instantiated child progress object into the `addChild(child:withPendingUnitCount:)` method. In this alternative, we add a convenience method that bears the function signature `makeChild(pendingUnitCount:)` to the `Progress` class. This method instantiates an empty progress and adds itself as a child, allowing developers to add a child progress to a parent progress without having to instantiate a child progress themselves. The additional method reads as follows: ```swift extension Progress { @@ -980,10 +1426,13 @@ We considered adding a `Task.isCancelled` check in the `complete(count:)` method We considered using `UInt64` as the type for `totalCount` and `completedCount` to support the case where developers use `totalCount` and `completedCount` to track downloads of larger files on 32-bit platforms byte-by-byte. However, developers are not encouraged to update progress byte-by-byte, and should instead set the counts to the granularity at which they want progress to be visibly updated. For instance, instead of updating the download progress of a 10,000 bytes file in a byte-by-byte fashion, developers can instead update the count by 1 for every 1,000 bytes that has been downloaded. In this case, developers set the `totalCount` to 10 instead of 10,000. To account for cases in which developers may want to report the current number of bytes downloaded, we added `totalByteCount` and `completedByteCount` to `ProgressManager.Properties`, which developers can set and display using format style. ### Make `totalCount` a settable property on `ProgressManager` -We previously considered making `totalCount` a settable property on `ProgressManager`, but this would introduce a race condition that is common among cases in which `Sendable` types have settable properties. This is because two threads can try to mutate `totalCount` at the same time, but since `ProgressManager` is `Sendable`, we cannot guarantee the order of how the operations will interleave, thus creating a race condition. This results in `totalCount` either reflecting both the mutations, or one of the mutations indeterministically. Therefore, we changed it so that `totalCount` is a read-only property on `ProgressManager`, and is only mutable within the `withProperties` closure to prevent this race condition. +We previously considered making `totalCount` a settable property on `ProgressManager`, but this would introduce a race condition that is common among cases in which `Sendable` types have settable properties. This is because two threads can try to mutate `totalCount` at the same time, but since `ProgressManager` is `Sendable`, we cannot guarantee the order of how the operations will interleave, thus creating a race condition. This results in `totalCount` either reflecting both the mutations, or one of the mutations indeterministically. Therefore, we changed it so that `totalCount` is a read-only property on `ProgressManager`, and is only mutable within the `setCounts` closure to prevent this race condition. + +### Representation of indeterminate state in `ProgressManager` +There were discussions about representing indeterminate state in `ProgressManager` alternatively, for example, using enums. However, since `totalCount` is an optional and can be set to `nil` to represent indeterminate state, we think that this is straightforward and sufficient to represent indeterminate state for cases where developers do not know `totalCount` at the start of an operation they want to report progress for. A `ProgressManager` becomes determinate once its `totalCount` is set to an `Int`. -### Representation of Indeterminate state in `ProgressManager` -There were discussions about representing indeterminate state in `ProgressManager` alternatively, for example, using enums. However, since `totalCount` is an optional and can be set to `nil` to represent indeterminate state, we think that this is straightforward and sufficient to represent indeterminate state for cases where developers do not know `totalCount` at the start of an operation they want to report progress for. A `ProgressManager` becomes determinate once its `totalCount` is set to an `Int`. +### Allow declared custom additional property to be any type that can be casted as `any Sendable` +We initially allowed the full flexibility of allowing developers to declare `ProgressManager.Property` types to be of any type, including structs. However, we realized that this has a severely negative impact on performance of the API. Thus, for now, we allow developers to only declare `ProgressManager.Property` with only certain `Value` and `Summary` types. ## Acknowledgements Thanks to