From 82c0eba291bc0a1ea2ae55f95f61f22ad671f48e Mon Sep 17 00:00:00 2001 From: Chloe Yeo Date: Tue, 25 Feb 2025 13:35:30 -0800 Subject: [PATCH 01/29] ProgressReporter proposal --- Proposals/NNNN-progress-reporter.md | 995 ++++++++++++++++++++++++++++ 1 file changed, 995 insertions(+) create mode 100644 Proposals/NNNN-progress-reporter.md diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/NNNN-progress-reporter.md new file mode 100644 index 000000000..32efb4fb2 --- /dev/null +++ b/Proposals/NNNN-progress-reporter.md @@ -0,0 +1,995 @@ +# `ProgressReporter`: Progress Reporting in Swift Concurrency + +* Proposal: SF-NNNN +* Author(s): [Chloe Yeo](https://github.com/chloe-yeo) +* Review Manager: TBD +* Status: **Draft** + +## Revision history + +* **v1** Initial version + +## Table of Contents + +* [Introduction](#introduction) +* [Motivation](#motivation) +* [Proposed Solution and Example](#proposed-solution-and-example) + * [Reporting Progress with Identical Properties](#reporting-progress-with-identical-properties) + * [Reporting Progress with Distinct Properties](#reporting-progress-with-distinct-properties) + * [Reporting Progress with Task Cancellation](#reporting-progress-with-task-cancellation) + * [Advantages of using `ProgressReporter.Progress` as Currency Type](#advantages-of-using-progresssreporterprogress-as-currency-type) + * [Interoperability with Foundation\'s `Progress`](#interoperability-with-foundations-progress) +* [Detailed Design](#detailed-design) + * [`ProgressProperties`](#progressproperties) + * [`BasicProgressProperties`](#basicprogressproperties) + * [`FileProgressProperties`](#fileprogressproperties) + * [`ProgressReporter`](#progressreporter) + * [`ProgressReporter.Progress`](#progressreporterprogress) + * [Interoperability with Foundation's `Progress`](#methods-for-interoperability-with-foundations-progress) + * [`ProgressReporter` \(Parent\) \- `Progress` \(Child\)](#progressreporter-parent---progress-child) + * [`Progress` \(Parent\) \- `ProgressReporter` \(Child\)](#progress-parent---progressreporter-child) +* [Impact on Existing Code](#impact-on-existing-code) +* [Future Directions](#future-directions) + * [ProgressView Overload in SwiftUI](#progressview-overload-in-swiftui) + * [Distributed ProgressReporter](#distributed-progressreporter) +* [Alternatives Considered](#alternatives-considered) + * [Alternative Names](#alternative-names) + * [Introduce `ProgressReporter` to Swift standard library](#introduce-progressreporter-to-swift-standard-library) + * [Implement `ProgressReporter` as an actor](#implement-progressreporter-as-an-actor) + * [Implement `ProgressReporter` as a protocol](#implement-progressreporter-as-a-protocol) + * [Introduce an Observable adapter for `ProgressReporter`](#introduce-an-observable-adapter-for-progressreporter) + * [Introduce Support for Cancellation, Pausing, Resuming of `ProgressReporter`](#introduce-support-for-cancellation-pausing-and-resuming-of-progressreporter) + * [Move totalCount and completedCount properties to `ProgressProperties` protocol](#move-totalcount-and-completedcount-properties-to-progressproperties-protocol) + * [Introduce totalCount and completedCount properties as UInt64](#introduce-totalcount-and-completedcount-properties-as-uint64) + * [Store Foundation\'s `Progress` in TaskLocal Storage](#store-foundations-progress-in-tasklocal-storage) + * [Add Convenience Method to Foundation\'s `Progress` for Easier Instantiation of Child Progress](#add-convenience-method-to-foundations-progress-for-easier-instantiation-of-child-progress) +* [Acknowledgements](#acknowledgements) + +## Introduction + +Progress reporting is a generally useful concept, and can be helpful in all kinds of applications: from high level UIs, to simple command line tools, and more. + +Foundation offers a progress reporting mechanism that has been very popular with application developers on Apple platforms. The existing `Progress` class provides a self-contained, tree-based mechanism for progress reporting and is adopted in various APIs which are able to report progress. The functionality of the `Progress` class is two-fold –– it reports progress at the code level, and at the same time, displays progress at the User Interface level. While the recommended usage pattern of `Progress` works well with Cocoa's completion-handler-based async APIs, it does not fit well with Swift's concurrency support via async/await. + +This proposal aims to introduce an efficient, easy-to-use, less error-prone Progress Reporting API —— `ProgressReporter` —— that is compatible with async/await style concurrency to Foundation. To further support the use of this Progress Reporting API with high-level UIs, this API is also `Observable`. + +## Motivation + +A progress reporting mechanism that is compatible with Swift's async/await style concurrency would be to pass a `Progress` instance as a parameter to functions or methods that report progress. The current recommended usage pattern of Foundation's `Progress`, as outlined in [Apple Developer Documentation](https://developer.apple.com/documentation/foundation/progress), does not fit well with async/await style concurrency. Typically, a function that aims to report progress to its callers will first return an instance of Foundation's `Progress`. The returned instance is then added as a child to a parent `Progress` instance. + +In the following example, the function `chopFruits(completionHandler:)` reports progress to its caller, `makeSalad()`. + +```swift +public func makeSalad() { + let progress = Progress(totalUnitCount: 3) // parent Progress instance + let subprogress = chopFruits { result in // child Progress instance + switch result { + case .success(let progress): + progress.completedUnitCount += 1 + case .failure(let error): + print("Fruits not chopped") + } + } + progress.addChild(subprogress, withPendingUnitCount: 1) +} + +public func chopFruits(completionHandler: @escaping (Result) -> Void) -> Progress {} +``` + +When we update this function to use async/await, the previous pattern no longer composes as expected: + +```swift +public func makeSalad() async { + let progress = Progress(totalUnitCount: 3) + let chopSubprogress = await chopFruits() + progress.addChild(chopSubprogress, withPendingUnitCount: 1) +} + +public func chopFruits() async -> Progress {} +``` + +The previous pattern of "returning" the `Progress` instance no longer composes as expected because we are forced to await the `chopFruits()` call. We could _then_ return the `Progress` instance. However, the `Progress` instance that gets returned already has its `completedUnitCount` equal to `totalUnitCount`. This defeats its purpose of showing incremental progress as the code runs to completion within the method. + +Additionally, while it may be possible to reuse Foundation's `Progress` to report progress in an `async` function by passing `Progress` as an argument to the function reporting progress, it is more error-prone, as shown below: + +```swift +let fruits = ["apple", "orange", "melon"] +let vegetables = ["spinach", "carrots", "celeries"] + +public func makeSalad() async { + let progress = Progress(totalUnitCount: 2) + + let choppingProgress = Progress() + progress.addChild(subprogress, withPendingUnitCount: 1) + + await chopFruits(progress: subprogress) + + await chopVegetables(progress: subprogress) // Author's Mistake, same subprogress was passed! +} + +public func chopFruits(progress: Progress) async { + progress.totalUnitCount = Int64(fruits.count) + for fruit in fruits { + await chopItem(fruit) + progress.completedUnitCount += 1 + } +} + +public func chopVegetables(progress: Progress) async { + progress.totalUnitCount = Int64(vegetables.count) // Author's Mistake, overrides progress made in chopFruits as same subprogress was passed! + for vegetable in vegetables { + await chopItem(vegetable) + progress.completedUnitCount += 1 + } +} + +public func chopItem(_ item: String) async {} +``` + +The existing `Progress` in Foundation was not designed in a way that enforces the usage of `Progress` instance as a function parameter to report progress. Without a strong rule about who creates the `Progress` and who consumes it, it is easy to end up in a situation where the `Progress` is used more than once. This results in nondeterministic behavior when developers may accidentally overcomplete or override a `Progress` instance. + +In contrast, the introduction of a new progress reporting mechanism following the new `ProgressReporter` type would enforce safer practices of progress reporting via a strong rule of what should be passed as parameter and what should be used to report progress. + +This proposal outlines the use of `ProgressReporter` as reporters of progress and `~Copyable` `ProgressReporter.Progress` as parameters passed to progress reporting methods. + +## Proposed solution and example + +Before proceeding further with this proposal, it is important to keep in mind the type aliases introduced with this API. The examples outlined in the following sections will utilize type aliases as follows: + +```swift +public typealias BasicProgressReporter = ProgressReporter +public typealias FileProgressReporter = ProgressReporter + +public typealias FileProgress = ProgressReporter.Progress +public typealias BasicProgress = ProgressReporter.Progress +``` + +### Reporting Progress With Identical Properties + +To begin, let's create a class called `MakeSalad` that reports progress made on a salad while it is being made. + +```swift +struct Fruit { + let name: String + + init(_ fruit: String) { + self.name = fruit + } + + func chop() async {} +} + +struct Dressing { + let name: String + + init (_ dressing: String) { + self.name = dressing + } + + func pour() async {} +} + +public class MakeSalad { + + let overall: BasicProgressReporter + let fruits: [Fruit] + let dressings: [Dressing] + + public init() { + overall = BasicProgressReporter(totalCount: 100) + fruits = [Fruit("apple"), Fruit("banana"), Fruit("cherry")] + dressings = [Dressing("mayo"), Dressing("mustard"), Dressing("ketchup")] + } +} +``` + +In order to report progress on subparts of making a salad, such as `chopFruits` and `mixDressings`, we can instantitate subprogresses by passing an instance of `ProgressReporter.Progress` to each subpart. Each `ProgressReporter.Progress` passed into the subparts then have to be consumed to initialize an instance of `ProgressReporter`. This is done by calling `reporter(totalCount:)` on `ProgressReporter.Progress`. These child progresses will automatically contribute to the `overall` progress reporter within the class, due to established parent-children relationships between `overall` and the reporters of subparts. This can be done as follows: + +```swift +extension MakeSalad { + + public func start() async -> String { + // Gets a BasicProgress instance with 70 portioned count from `overall` + let fruitsProgress = overall.assign(count: 70) + await chopFruits(progress: fruitsProgress) + + // Gets a BasicProgress instance with 30 portioned count from `overall` + let dressingsProgress = overall.assign(count: 30) + await mixDressings(progress: dressingsProgress) + + return "Salad is ready!" + } + + private func chopFruits(progress: consuming BasicProgress?) async { + // Initializes a progress reporter to report progress on chopping fruits + // with passed-in progress parameter + let choppingReporter = progress?.reporter(totalCount: fruits.count) + for fruit in fruits { + await fruit.chop() + choppingReporter?.complete(count: 1) + } + } + + private func mixDressings(progress: consuming BasicProgress?) async { + // Initializes a progress reporter to report progress on mixing dressing + // with passed-in progress parameter + let dressingReporter = progress?.reporter(totalCount: dressings.count) + for dressing in dressings { + await dressing.pour() + dressingReporter?.complete(count: 1) + } + } +} +``` + +### Reporting Progress With Distinct Properties + +`ProgressReporter`, which is a generic class, allows developers to define their own type of `ProgressProperties`, and report progress with additional metadata or properties. We propose adding `BasicProgressProperties` for essential use cases and a `FileProgressProperties` for reporting progress on file-related operations. Developers can create progress trees in which all instances of `ProgressReporter` are of the same kind, or a mix of `ProgressReporter` instances with different `ProgressProperties`. + +In this section, we will show an example of how progress reporting with different kinds of `ProgressReporter` can be done. To report progress on both making salad and downloading images, developers can use both `BasicProgressReporter` and `FileProgressReporter` which are children reporters to an overall `BasicProgressReporter`, as follows: + +```swift +struct Fruit { + let name: String + + init(_ fruit: String) { + self.name = fruit + } + + func chop() async {} +} + +struct Image { + + let bytes: Int + + init(bytes: Int) { + self.bytes = bytes + } + + func read() async {} +} + +class Multitask { + + let overall: BasicProgressReporter + // These are stored in this class to keep track of + // additional properties of reporters of chopFruits and downloadImages + var chopFruits: BasicProgressReporter? + var downloadImages: FileProgressReporter? + let fruits: [Fruit] + let images: [Image] + + init() { + overall = BasicProgressReporter(totalCount: 100) + fruits = [Fruit("apple"), Fruit("banana"), Fruit("cherry")] + images = [Image(bytes: 1000), Image(bytes: 2000), Image(bytes: 3000)] + } + + func chopFruitsAndDownloadImages() async { + // Gets a BasicProgress instance with 50 portioned count from `overall` + let chopProgress = overall.assign(count: 50) + await chop(progress: chopProgress) + + // Gets a FileProgress instance with 50 portioned count from `overall` + let downloadProgress = overall.assign(count: 50, kind: FileProgressProperties.self) + await download(progress: downloadProgress) + } +} +``` + +Here's how you can compose two different kinds of progress into the same tree, with `overall` being the top-level `ProgressReporter`. `overall` has two children — `chopFruits` of Type `BasicProgressReporter`, and `downloadImages` of Type `FileProgressReporter`. You can report progress to both `chopFruits` and `downloadImages` as follows: + +```swift +extension Multitask { + + func chop(progress: consuming BasicProgress?) async { + // Initializes a BasicProgressReporter to report progress on chopping fruits + // with passed-in `progress` parameter + chopReporter = progress?.reporter(totalCount: fruits.count) + for fruit in fruits { + await fruit.chop() + chopReporter?.complete(count: 1) + } + } + + func download(progress: consuming FileProgress?) async { + // Initializes a FileProgressReporter to report progress on file downloads + // with passed-in `progress` parameter + downloadReporter = progress?.reporter(totalCount: images.count, properties: FileProgressProperties()) + for image in images { + if let reporter = downloadReporter { + // Passes in a FileProgress instance with 1 portioned count from `downloadImages` + await read(image, progress: reporter.assign(count: 1)) + } + } + } + + func read(_ image: Image, progress: consuming FileProgress?) async { + // Instantiates a FileProgressProperties with known properties + // to be passed into `reporter(totalCount: properties:)` + let fileProperties = FileProgressProperties(totalFileCount: 1, totalByteCount: image.bytes) + + // Initializes a FileProgressReporter with passed-in `progress` parameter + let readFile = progress?.reporter(totalCount: 1, properties: fileProperties) + + // Initializes other file-related properties of `readFile` that are only obtained later + readFile?.properties.throughput = calculateThroughput() + readFile?.properties.estimatedTimeRemaining = calculateEstimatedTimeRemaining() + + await image.read() + + // Updates file-related properties of `readFile` + readFile?.properties.completedFileCount += 1 + readFile?.properties.completedByteCount += image.bytes + + // Completes `readFile` entirely + readFile?.complete(count: 1) + } +} +``` + +### Reporting Progress with Task Cancellation + +A `ProgressReporter` running in a `Task` can respond to the cancellation of the `Task`. In structured concurrency, cancellation of the parent task results in the cancellation of all child tasks. Mirroring this behavior, a `ProgressReporter` running in a parent `Task` that is cancelled will have its children instances of `ProgressReporter` cancelled as well. + +Cancellation in the context of `ProgressReporter` means that any subsequent calls to `complete(count:)` after a `ProgressReporter` is cancelled results in a no-op. Trying to update 'cancelled' `ProgressReporter` and its children will no longer increase `completedCount`, thus no further forward progress will be made. + +While the code can continue running, calls to `complete(count:)` from a `Task` that is cancelled will result in a no-op, as follows: + +```swift +let fruits = ["apple", "banana", "cherry"] +let overall = BasicProgressReporter(totalCount: fruits.count) + +func chopFruits(_ fruits: [String]) async -> [String] { + await withTaskGroup { group in + + // Concurrently chop fruits + for fruit in fruits { + group.addTask { + await FoodProcessor.chopFruit(fruit: fruit, progress: overall.assign(count: 1)) + } + if fruit == "banana" { + group.cancelAll() + } + } + + // Collect chopped fruits + var choppedFruits: [String] = [] + for await choppedFruit in group { + choppedFruits.append(choppedFruit) + } + + return choppedFruits + } +} + +class FoodProcessor { + static func chopFruit(fruit: String, progress: consuming BasicProgress?) async -> String { + let progressReporter = progress?.reporter(totalCount: 1) + ... // expensive async work here + progressReporter?.complete(count: 1) // This becomes a no-op if the Task is cancelled + return "Chopped \(fruit)" + } +} +``` + +### Advantages of using `ProgresssReporter.Progress` as Currency Type + +The advantages of `ProgressReporter` mainly derive from the use of `ProgressReporter.Progress` as a currency to create descendants of `ProgresssReporter`, and the recommended ways to use `ProgressReporter.Progress` are as follows: + +1. Pass `ProgressReporter.Progress` instead of `ProgressReporter` as a parameter to methods that report progress. + +`ProgressReporter.Progress` should be used as the currency to be passed into progress-reporting methods, within which a child `ProgressReporter` instance that constitutes a portion of its parent's total units is created via a call to `reporter(totalCount:)`, as follows: + +```swift +func testCorrectlyReportToSubprogressAfterInstantiatingReporter() async { + let overall = BasicProgressReporter(totalCount: 2) + await subTask(progress: overall.assign(count: 1)) +} + +func subTask(progress: consuming BasicProgress?) async { + let count = 10 + let progressReporter = progress?.reporter(totalCount: count) // returns an instance of ProgressReporter that can be used to report subprogress + for _ in 1...count { + progressReporter?.complete(count: 1) // reports progress as usual + } +} +``` + +While developers may accidentally make the mistake of trying to report progress to a passed-in `ProgressReporter.Progress`, the fact that it does not have the same properties as an actual `ProgressReporter` means the compiler can inform developers when they are using either `ProgressReporter` or `ProgressReporter.Progress` wrongly. The only way for developers to kickstart actual progress reporting with `ProgressReporter.Progress` is by calling the `reporter(totalCount:)` to create a `ProgressReporter`, then subsequently call `complete(count:)` on `ProgressReporter`. + +Each time before progress reporting happens, there needs to be a call to `reporter(totalCount:)`, which returns a `ProgressReporter` instance, before calling `complete(count:)` on the returned `ProgressReporter`. + +The following faulty example shows how reporting progress directly to `ProgressReporter.Progress` without initializing it will be cause a compiler error. Developers will always need to instantiate a `ProgressReporter` from `ProgresReporter.Progress` before reporting progress. + +```swift +func testIncorrectlyReportToSubprogressWithoutInstantiatingReporter() async { + let overall = BasicProgressReporter(totalCount: 2) + await subTask(progress: overall.assign(count: 1)) +} + +func subTask(progress: consuming BasicProgress?) async { + // COMPILER ERROR: Value of type 'BasicProgress' (aka 'ProgressReporter.Progress') has no member 'complete' + progress?.complete(count: 1) +} +``` + +2. Consume each `ProgressReporter.Progress` only once, and if not consumed, its parent `ProgressReporter` behaves as if none of its units were ever allocated to create `ProgressReporter.Progress`. + +Developers should create only one `ProgressReporter.Progress` for a corresponding to-be-instantiated `ProgressReporter` instance, as follows: + +```swift +func testCorrectlyConsumingSubprogress() { + let overall = BasicProgressReporter(totalCount: 2) + + let progressOne = overall.assign(count: 1) // create one ProgressReporter.Progress + let reporterOne = progressOne.reporter(totalCount: 10) // initialize ProgressReporter instance with 10 units + + let progressTwo = overall.assign(count: 1) //create one ProgressReporter.Progress + let reporterTwo = progressTwo.reporter(totalCount: 8) // initialize ProgressReporter instance with 8 units +} +``` + +It is impossible for developers to accidentally consume `ProgressReporter.Progress` more than once, because even if developers accidentally **type** out an expression to consume an already-consumed `ProgressReporter.Progress`, their code won't compile at all. + +The `reporter(totalCount:)` method, which **consumes** the `ProgressReporter.Progress`, can only be called once on each `ProgressReporter.Progress` instance. If there are more than one attempts to call `reporter(totalCount:)` on the same instance of `ProgressReporter.Progress`, the code will not compile due to the `~Copyable` nature of `ProgressReporter.Progress`. + +```swift +func testIncorrectlyConsumingSubprogress() { + let overall = BasicProgressReporter(totalCount: 2) + + let progressOne = overall.assign(count: 1) // create one BasicProgress + let reporterOne = progressOne.reporter(totalCount: 10) // initialize ProgressReporter instance with 10 units + + // COMPILER ERROR: 'progressOne' consumed more than once + let reporterTwo = progressOne.reporter(totalCount: 8) // initialize ProgressReporter instance with 8 units using same Progress +} +``` + +### Interoperability with Foundation's `Progress` + +In both cases below, the propagation of progress of subparts to a root progress should work the same ways Foundation's `Progress` and `ProgressReporter` work. + +Consider two progress reporting methods, one which utilizes Foundation's `Progress`, and another using `ProgressReporter`: + +```swift +// Framework code: Function reporting progress with Foundation's `Progress` +func doSomethingWithProgress() -> Progress { + let p = Progress.discreteProgress(totalUnitCount: 2) + Task.detached { + // do something + p.completedUnitCount = 1 + // do something + p.completedUnitCount = 2 + } + return p +} + +// Framework code: Function reporting progress with `ProgressReporter` +func doSomethingWithReporter(progress: consuming BasicProgress?) async -> Int { + let reporter = progress?.reporter(totalCount: 2) + //do something + reporter?.complete(count: 1) + //do something + reporter?.complete(count: 1) +} +``` + +In the case in which we need to receive a `Progress` instance and add it as a child to a `ProgressReporter` parent, we can use the interop method `assign(count: to:)`. + +The choice of naming the interop method as `assign(count: to:)` is to keep the syntax consistent with the method used to add a `ProgressReporter` instance to the progress tree, `assign(count:)`. An example of how these can be used to compose a `ProgressReporter` tree with a top-level `ProgressReporter` is as follows: + +```swift +// Developer code +func testProgressReporterParentProgressChildInterop() async { + let overall = BasicProgressReporter(totalCount: 2) // Top-level `ProgressReporter` + + // Assigning 1 unit of overall's `totalCount` to `ProgressReporter.Progress` + let progressOne = overall.assign(count: 1) + // Passing `ProgressReporter.Progress` to method reporting progress + let result = await doSomethingWithReporter(progress: progressOne) + + + // Getting a Foundation's `Progress` from method reporting progress + let progressTwo = doSomethingWithProgress() + // Assigning 1 unit of overall's `totalCount` to Foundation's `Progress` + overall.assign(count: 1, to: progressTwo) +} +``` + +The reverse case, in which a framework needs to receive a `ProgressReporter` instance as a child from a top-level `Progress`, can also be done. The interop method `makeChild(withPendingUnitCount: kind:)` added to `Progress` will support the explicit composition of a progress tree. + +The choice of naming the interop method as `makeChild(withPendingUnitCount: kind:)` is to keep the syntax consistent with the method used to add a `Foundation.Progress` instance as a child, `addChild(_: withPendingUnitCount:)`. An example of how this can be used to compose a `Foundation.Progress` tree with a top-level `Foundation.Progress` is as follows: + +```swift +// Developer code +func testProgressParentProgressReporterChildInterop() { + let overall = Progress(totalUnitCount: 2) // Top-level Foundation's `Progress` + + // Getting a Foundation's `Progress` from method reporting progress + let progressOne = doSomethingWithProgress() + // Add Foundation's `Progress` as a child which takes up 1 unit of overall's `totalUnitCount` + overall.addChild(progressOne, withPendingUnitCount: 1) + + // Getting a `ProgressReporter.Progress` which takes up 1 unit of overall's `totalUnitCount` + let progressTwo = overall.makeChild(withPendingUnitCount: 1, kind: BasicProgressProperties.self) + // Passing `ProgressReporter.Progress` instance to method reporting progress + doSomethingWithReporter(progress: progressTwo) +} +``` + +## Detailed design + +### `ProgressProperties` + +The `ProgressProperties` protocol allows you to specify additional properties of a `ProgressReporter` instance. You can create conforming types that contains additional properties on top of the required properties and methods, and these properties further customize `ProgressReporter` and provide more information in localized descriptions returned. + +In addition to specifying additional properties, the `ProgressProperties` protocol also allows you to specify `LocalizedDescriptionOptions` as options for localized descriptions of a `ProgressReporter` provided to observers. + +```swift +/// `ProgressProperties` is a protocol that defines the requirement for any type of `ProgressReporter`. +@available(FoundationPreview 6.2, *) +public protocol ProgressProperties : Hashable, Sendable { + + /// Returns a new instance of Self which represents the aggregation of an array of Self. + /// + /// Default implementation returns Self as it assumes that there are no children. + /// - Parameter children: An array of Self to be aggregated. + /// - Returns: An instance of Self. + func reduce(children: [Self]) -> Self + + /// A struct containing options for specifying localized description. + associatedtype LocalizedDescriptionOptions : Hashable, Equatable + + /// Returns a `LocalizedStringResource` for a `ProgressReporter` instance based on `LocalizedDescriptionOptions` specified. + /// - Parameters: + /// - progress: `ProgressReporter` instance to generate localized description for. + /// - options: A set of `LocalizedDescriptionOptions` to include in localized description. + /// - Returns: A `LocalizedStringResource`. + func localizedDescription(_ progress: ProgressReporter, _ options: Set) -> LocalizedStringResource +} +``` + +There are two implemented conforming types of `ProgressProperties`, namely: +1. `BasicProgressProperties`: No additional properties +2. `FileProgressProperties`: Additional properties for progress on file-related operations + +#### `BasicProgressProperties` + +```swift +/// A basic implementation of ProgressProperties that contains no additional properties. +@available(FoundationPreview 6.2, *) +public struct BasicProgressProperties : ProgressProperties { + + /// Initializes an instance of `BasicProgressProperties`. + public init() + + /// Returns `self`. This is because there are no properties in `BasicProgressProperties`. + /// - Parameter children: An array of children of the same `Type`. + /// - Returns: `self` + public func reduce(children: [Self]) -> Self { + return self + } + + /// A struct containing all options to choose in specifying how localized description should be generated. + public struct LocalizedDescriptionOptions: Sendable, Hashable, Equatable { + + /// Option to include formatted `fractionCompleted` in localized description. + /// Example: 20% completed. + /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. + /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. + public static func percentage(_ style: FloatingPointFormatStyle.Percent?) -> LocalizedDescriptionOptions + + /// Option to include formatted `completedCount` / `totalCount` in localized description. + /// Example: 5 of 10 + /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. + /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. + public static func count(_ style: IntegerFormatStyle?) -> LocalizedDescriptionOptions + } + + /// Returns a `LocalizedStringResource` based on options provided. + /// + /// Examples of localized description that can be generated include: + /// 20% completed + /// 2 of 10 + /// 2 of 10 - 20% completed + /// + /// - Parameters: + /// - progress: `ProgressReporter` instance to generate localized description for. + /// - options: A set of `LocalizedDescriptionOptions` to specify information to be included in localized description. + /// - Returns: A `LocalizedStringResource`. + public func localizedDescription(_ progress: ProgressReporter, _ options: Set) -> LocalizedStringResource +} +``` + +#### `FileProgressProperties` + +```swift +/// A custom `ProgressProperties` to incorporate additional properties such as `totalFileCount` to +/// ProgressReporter, which itself tracks only general properties such as `totalCount`. +@available(FoundationPreview 6.2, *) +public struct FileProgressProperties : ProgressProperties { + + /// Initializes an instance of `FileProgressProperties` with all fields as `nil` or defaults. + public init() + + /// Initializes an instance of `FileProgressProperties`. + /// - Parameters: + /// - totalFileCount: Total number of files. + /// - totalByteCount: Total number of bytes. + /// - completedFileCount: Completed number of files. + /// - completedByteCount: Completed number of bytes. + /// - throughput: Throughput in bytes per second. + /// - estimatedTimeRemaining: A `Duration` representing amount of time remaining to completion. + public init(totalFileCount: Int?, totalByteCount: UInt64?, completedFileCount: Int = 0, completedByteCount: UInt64 = 0, throughput: UInt64? = nil, estimatedTimeRemaining: Duration? = nil) + + /// An Int representing total number of files. + public var totalFileCount: Int? + + /// An Int representing completed number of files. + public var completedFileCount: Int + + /// A UInt64 representing total bytes. + public var totalByteCount: UInt64? + + /// A UInt64 representing completed bytes. + public var completedByteCount: UInt64 + + /// A UInt64 representing throughput in bytes per second. + public var throughput: UInt64? + + /// A Duration representing amount of time remaining in the processing of files. + public var estimatedTimeRemaining: Duration? + + /// Returns a new `FileProgressProperties` instance that is a result of aggregating an array of children`FileProgressProperties` instances. + /// - Parameter children: An Array of `FileProgressProperties` instances to be aggregated into a new `FileProgressProperties` instance. + /// - Returns: A `FileProgressProperties` instance. + public func reduce(children: [FileProgressProperties]) -> FileProgressProperties + + /// A struct containing all options to choose in specifying how localized description should be generated. + public struct LocalizedDescriptionOptions: Sendable, Hashable, Equatable { + + /// Option to include formatted `fractionCompleted` in localized description. + /// Example: 20% completed. + /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. + /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. + public static func percentage(_ style: FloatingPointFormatStyle.Percent?) -> LocalizedDescriptionOptions { + return LocalizedDescriptionOptions("Percentage", formatPercentage: style, formatCount: nil) + } + + /// Option to include formatted `completedCount` / `totalCount` in localized description. + /// Example: 5 of 10 + /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. + /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. + public static func count(_ style: IntegerFormatStyle?) -> LocalizedDescriptionOptions { + return LocalizedDescriptionOptions("Percentage", formatPercentage: nil, formatCount: style) + } + + /// Option to include `completedFileCount` / `totalFileCount` in localized description. + /// Example: 1 of 5 files + /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedFileCount` and `totalFileCount`. + /// - Returns: A `LocalizedStringResource` for formatted `completedFileCount` / `totalFileCount`. + public static func files(_ style: IntegerFormatStyle?) -> LocalizedDescriptionOptions + + /// Option to include formatted `completedByteCount` / `totalByteCount` in localized description. + /// Example: Zero kB of 123.5 MB + /// - Parameter style: A `ByteCountFormatStyle` instance that should be used to format `completedByteCount` and `totalByteCount`. If this is `nil`, it defaults to `ByteCountFormatStyle(style: .file, allowedUnits: .all, spellsOutZero: true, includesActualByteCount: false, locale: .autoupdatingCurrent)`. + /// - Returns: A `LocalizedDescriptionOption` for formatted `completedByteCount` / `totalByteCount`. + public static func bytes(_ style: ByteCountFormatStyle?) -> LocalizedDescriptionOptions + + /// Option to include formatted `throughput` (bytes per second) in localized description. + /// Example: 10 MB/s + /// - Parameter style: `ByteCountFormatStyle` instance used to format `completedByteCount` and `totalByteCount`. If this is `nil`, it defaults to `ByteCountFormatStyle(style: .file, allowedUnits: .all, spellsOutZero: true, includesActualByteCount: false, locale: .autoupdatingCurrent)`. + /// - Returns: A `LocalizedDescriptionOption` for formatted `throughput`. + public static func throughput(_ style: ByteCountFormatStyle?) -> LocalizedDescriptionOptions + + /// Option to include formatted `estimatedTimeRemaining` in localized description. + /// Example: 5 minutes remaining + /// - Parameter style: `Duration.UnitsFormatStyle` instance used to format `estimatedTimeRemaining`, which is of `Duration` Type. If this is `nil`, it defaults to `Duration.UnitsFormatStyle(allowedUnits: Set(arrayLiteral: .hours, .minutes), width: .wide)`. + /// - Returns: A `LocalizedDescriptionOption` for formatted `estimatedTimeRemaining`. + public static func estimatedTimeRemaining(_ style: Duration.UnitsFormatStyle?) -> LocalizedDescriptionOptions + } + + /// Returns a custom `LocalizedStringResource` for file-related `ProgressReporter` of `FileProgressProperties` based on the selected `LocalizedDescriptionOptions`. + /// Examples of localized description that can be generated include: + /// 20% completed + /// 5 of 10 files + /// 2 minutes remaining + /// 2 of 10 - 20% completed + /// + /// - Parameters: + /// - progress: `ProgressReporter` instance to generate localized description for. + /// - options: A set of `LocalizedDescriptionOptions` to specify information to be included in localized description. + /// - Returns: A `LocalizedStringResource`. + public func localizedDescription(_ progress: ProgressReporter, _ options: Set) -> LocalizedStringResource +} +``` + +### `ProgressReporter` + +`ProgressReporter` serves as a generic interface for users to instantiate progress reporting, which can be characterized further using custom `Properties` created by developers. An instance of `ProgressReporter` can be used to either track progress of a single task, or track progress of a tree of `ProgressReporter` instances. + +```swift +/// Typealiases for ProgressReporter +public typealias BasicProgressReporter = ProgressReporter +public typealias FileProgressReporter = ProgressReporter + +/// Typealiases for ProgressReporter.Progress +public typealias BasicProgress = ProgressReporter.Progress +public typealias FileProgress = ProgressReporter.Progress + +/// ProgressReporter is a Sendable class used to report progress in a tree structure. +@available(FoundationPreview 6.2, *) +@Observable public final class ProgressReporter : Sendable, Hashable, Equatable { + + /// Represents total count of work to be done. + /// Setting this to `nil` means that `self` is indeterminate, + /// and developers should later set this value to an `Int` value before using `self` to report progress for `fractionCompleted` to be non-zero. + public var totalCount: Int? { get set } + + /// Represents completed count of work. + /// If `self` is indeterminate, returns 0. + public var completedCount: Int { get } + + /// Represents the fraction completed of the current instance, + /// taking into account the fraction completed in its children instances if children are present. + /// If `self` is indeterminate, returns `0.0`. + public var fractionCompleted: Double { get } + + /// Represents whether work is completed, + /// returns `true` if completedCount >= totalCount. + public var isFinished: Bool { get } + + /// Represents whether `totalCount` is initialized to an `Int`, + /// returns `true` only if `totalCount == nil`. + public var isIndeterminate: Bool { get } + + /// Access point to additional properties such as `fileTotalCount` + /// declared within struct of custom type `ProgressProperties`. + public var properties: Properties { get set } + + /// Initializes `self` with `totalCount` and `properties`. + /// If `totalCount` is set to `nil`, `self` is indeterminate. + /// + /// - Parameters: + /// - totalCount: Total count of work. + /// - properties: An instance of`ProgressProperties`. + public convenience init(totalCount: Int?) + + /// Increases completedCount by `count`. + /// + /// This operation becomes a no-op if Task from which `self` gets created is cancelled. + /// - Parameter count: Number of units that `completedCount` should be incremented by. + public func complete(count: Int) + + /// Returns a `ProgressReporter.Progress` which can be passed to any method that reports progress. + /// + /// Delegates a portion of `self`'s `totalCount` to a to-be-initialized child `ProgressReporter` instance. + /// + /// - Parameter count: Count of units delegated to a child instance of `ProgressReporter` + /// which may be instantiated by calling `reporter(totalCount:)`. + /// - Parameter kind: `ProgressProperties` of child instance of `ProgressReporter`. + /// - Returns: A `ProgressReporter.Progress` instance. + public func assign(count: Int, kind: AssignedProperties.Type = AssignedProperties.self) -> ProgressReporter.Progress + + /// Overload for `assign(count: kind:)` for cases where + /// `ProgressReporter.Progress` has the same properties of `ProgressReporter`. + public func assign(count: Int) -> ProgressReporter.Progress + + /// Returns a `LocalizedStringResource` for `self`. + /// + /// Examples of localized descriptions that can be generated for `BasicProgressReporter` include: + /// 5 of 10 + /// 50% completed + /// 5 of 10 - 50% completed + /// + /// Examples of localized descriptions that can be generated for `FileProgressReporter` include: + /// 2 of 10 files + /// Zero kB of 123.5 MB + /// 2 minutes remaining + /// + /// - Parameter options: A set of `LocalizedDescriptionOptions` to include in localized description. + /// - Returns: A `LocalizedStringResource` based on `options`. + public func localizedDescription(including options: Set) -> LocalizedStringResource +} + +@available(FoundationPreview 6.2, *) +extension ProgressReporter where Properties == BasicProgressProperties { + + /// Initializes `self` with `totalCount` and `properties`. + /// If `totalCount` is set to `nil`, `self` is indeterminate. + /// + /// - Parameters: + /// - totalCount: Total count of work. + /// - properties: An instance of `BasicProgressProperties`. + public convenience init(totalCount: Int?, properties: BasicProgressProperties = BasicProgressProperties()) +} + +@available(FoundationPreview 6.2, *) +extension ProgressReporter where Properties == FileProgressProperties { + + /// Initializes `self` with `totalCount` and `properties`. + /// If `totalCount` is set to `nil`, `self` is indeterminate. + /// + /// - Parameters: + /// - totalCount: Total count of work. + /// - properties: An instance of `FileProgressProperties`. + public convenience init(totalCount: Int?, properties: FileProgressProperties = FileProgressProperties()) +} +``` + +### `ProgressReporter.Progress` + +An instance of `ProgressReporter.Progress` is returned from a call to `ProgressReporter`'s `assign(count: kind:)`. `ProgressReporter.Progress` acts as an intermediary instance that you pass into functions that report progress. Additionally, callers should convert `ProgressReporter.Progress` to `ProgressReporter` before starting to report progress with it by calling `reporter(totalCount:)`. + +```swift +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + + /// ProgressReporter.Progress is a nested ~Copyable struct used to establish parent-child relationship between two instances of ProgressReporter. + /// + /// ProgressReporter.Progress is returned from a call to `assign(count:)` by a parent ProgressReporter. + /// A child ProgressReporter is then returned by calling`reporter(totalCount:)` on a ProgressReporter.Progress. + public struct Progress : ~Copyable, Sendable { + + /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. + /// - Parameters: + /// - totalCount: Total count of returned child `ProgressReporter` instance. + /// - properties: An instance of conforming type of`ProgressProperties`. + /// - Returns: A `ProgressReporter` instance. + public consuming func reporter(totalCount: Int?, properties: Properties) -> ProgressReporter + } +} + +@available(FoundationPreview 6.2, *) +extension ProgressReporter.Progress where Properties == BasicProgressProperties { + + /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. + /// - Parameters: + /// - totalCount: Total count of returned child `ProgressReporter` instance. + /// - properties: An instance of `BasicProgressProperties`. + /// - Returns: A `ProgressReporter` instance. + public consuming func reporter(totalCount: Int?, properties: BasicProgressProperties = BasicProgressProperties()) -> ProgressReporter +} + +@available(FoundationPreview 6.2, *) +extension ProgressReporter.Progress where Properties == FileProgressProperties { + + /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. + /// - Parameters: + /// - totalCount: Total count of returned child `ProgressReporter` instance. + /// - properties: An instance of `FileProgressProperties`. + /// - Returns: A `ProgressReporter` instance. + public consuming func reporter(totalCount: Int?, properties: FileProgressProperties = FileProgressProperties()) -> ProgressReporter +} +``` + +### Methods for Interoperability with Foundation's `Progress` + +To allow frameworks which may have dependencies on the pre-existing progress-reporting protocol to adopt this new progress-reporting protocol, either as a recipient of a child `Progress` instance that needs to be added to its `ProgressReporter` tree, or as a provider of `ProgressReporter` that may later be added to another framework's `Progress` tree, there needs to be additional support for ensuring that progress trees can be composed with in two cases: +1. A `ProgressReporter` instance has to parent a `Progress` child +2. A `Progress` instance has to parent a `ProgressReporter` child + +#### ProgressReporter (Parent) - Progress (Child) + +To add an instance of `Progress` as a child to an instance of `ProgressReporter`, we pass an `Int` for the portion of `ProgressReporter`'s `totalCount` `Progress` should take up and a `Progress` instance to `assign(count: to:)`. The `ProgressReporter` instance will track the `Progress` instance just like any of its `ProgressReporter` children. + +```swift +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + // Adds a Foundation's `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. + /// - Parameters: + /// - count: Number of units delegated from `self`'s `totalCount`. + /// - progress: `Progress` which receives the delegated `count`. + public func assign(count: Int, to progress: Foundation.Progress) +} +``` + +#### Progress (Parent) - ProgressReporter (Child) + +To add an instance of `ProgressReporter` as a child to an instance of Foundation's `Progress`, the `Progress` instance calls `makeChild(count:kind:)` to get a `ProgressReporter.Progress` instance that can be passed as a parameter to a function that reports progress. The `Progress` instance will track the `ProgressReporter` instance as a child, just like any of its `Progress` children. + +```swift +@available(FoundationPreview 6.2, *) +extension Progress { + /// Returns a ProgressReporter.Progress which can be passed to any method that reports progress + /// and can be initialized into a child `ProgressReporter` to the `self`. + /// + /// Delegates a portion of totalUnitCount to a future child `ProgressReporter` instance. + /// + /// - Parameter count: Number of units delegated to a child instance of `ProgressReporter` + /// which may be instantiated by `ProgressReporter.Progress` later when `reporter(totalCount:)` is called. + /// - Returns: A `ProgressReporter.Progress` instance. + public func makeChild(withPendingUnitCount count: Int, kind: Kind.Type = Kind.self) -> ProgressReporter.Progress +} +``` + +## Impact on existing code + +There should be no impact on existing code, as this is an additive change. + +However, this new progress reporting API, `ProgressReporter`, which is compatible with Swift's async/await style concurrency, will be favored over the existing `Progress` API going forward. Depending on how widespread the adoption of `ProgressReporter` is, we may consider deprecating the existing `Progress` API. + +## Future Directions + +### `ProgressView` Overload in SwiftUI +To enable the usage of `ProgressReporter` for app development, an overload for SwiftUI's `ProgressView` will be added. SwiftUI's `ProgressView` currently can be intitialized using a `Double` or the existing `Progress` instance. Adding support to allow for use of `ProgressView` with `ProgressReporter` will enable adoption of `ProgressReporter` for app developers who wish to take advantage of `ProgressReporter` to do relatively extensive progress reporting and show progress on the User Interface. + +### Distributed `ProgressReporter` +To enable inter-process progress reporting, we would like to introduce distributed `ProgressReporter` in the future, which would functionally be similar to how Foundation's `Progress` mechanism for reporting progress across processes. + +## Alternatives considered + +### Alternative Names +As Foundation's `Progress` already exists, we had to come up with a name other than `Progress` for this API, but one that still conveys the progress-reporting functionality of this API. Some of the names we have considered are as follows: + +1. Alternative to `ProgressReporter` + - `AsyncProgress` + +We decided to proceed with the name `ProgressReporter` because prefixing an API with the term `Async` may be confusing for developers, as there is a precedent of APIs doing so, such as `AsyncSequence` adding asynchronicity to `Sequence`, whereas this is a different case for `ProgressReporter` vs `Progress`. + +2. Alternative to `ProgressReporter.Progress` + - `ProgressReporter.Link` + - `ProgressReporter.Child` + - `ProgressReporter.Token` + +While the names `Link`, `Child`, and `Token` may appeal to the fact that this is a type that is separate from the `ProgressReporter` itself and should only be used as a function parameter and to be consumed immediately to kickstart progress reporting, it is ambiguous because developers may not immedidately figure out its function from just the name itself. `Progress` is an intuitive name because developers will instinctively think of the term `Progress` when they want to adopt `ProgressReporting`. + +3. Alternative to `ProgressProperties` protocol + - `ProgressKind` + +While the name `ProgressKind` conveys the message that this is a protocol that developers should conform to when they want to create a different kind of `ProgressReporter`, the protocol mainly functions as a blueprint for developers to add additional properties to the existing properties such as `totalCount` and `completedCount` within `ProgressReporter`, so `ProgressProperties` reads more appropriately here. + +### Introduce `ProgressReporter` to Swift standard library +In consideration for making `ProgressReporter` a lightweight API for server-side developers to use without importing the entire `Foundation` framework, we considered either introducing `ProgressReporter` in a standalone module, or including `ProgressReporter` in existing Swift standard library modules such as `Observation` or `Concurrency`. However, given the fact that `ProgressReporter` has dependencies in `Observation` and `Concurrency` modules, and that the goal is to eventually support progress reporting over XPC connections, `Foundation` framework is the most ideal place to host the `ProgressReporter` as it is the central framework for APIs that provide core functionalities when these functionalities are not provided by Swift standard library and its modules. + +### Implement `ProgressReporter` as an actor +We considered implementing `ProgressReporter` as we want to maintain this API as a reference type that is safe to use in concurrent environments. However, if `ProgressReporter` were to be implemented, `ProgressReporter` will not be able to conform to `Observable` because actor-based keypaths do not exist as of now. Ensuring that `ProgressReporter` is `Observable` is important to us, as we want to ensure that `ProgressReporter` works well with UI components in SwiftUI. + +### Implement `ProgressReporter` as a protocol +In consideration of making the surface of the API simpler without the use of generics, we considered implementing `ProgressReporter` as a protocol, and provide implementations for specialized `ProgressReporter` classes that conform to the protocol, namely `BasicProgress`(`ProgressReporter` for progress reporting with only simple `count`) and `FileProgress` (`ProgressReporter` for progress reporting with file-related additional properties such as `totalFileCount`). This had the benefit of developers having to initialize a `ProgressReporter` instance with `BasicProgress(totalCount: 10)` instead of `ProgressReporter(totalCount: 10)`. + +However, one of the downside of this is that every time a developer wants to create a `ProgressReporter` that contains additional properties that are tailored to their use case, they would have to write an entire class that conforms to the `ProgressReporter` protocol from scratch, including the calculations of `fractionCompleted` for `ProgressReporter` trees. Additionally, the `~Copyable` struct nested within the `ProgressReporter` class that should be used as function parameter passed to functions that report progress will have to be included in the `ProgressReporter` protocol as an `associatedtype` that is `~Copyable`. However, the Swift compiler currently cannot suppress 'Copyable' requirement of an associated type and developers will need to consciously work around this. These create a lot of overload for developers wishing to report progress with additional metadata beyond what we provide in `BasicProgress` and `FileProgress` in this case. + +We decided to proceed with implementing `ProgressReporter` as a generic class to lessen the overhead for developers in customizing metadata for `ProgressReporter`, and at the same time introduce typealiases that simplify the API surface as follows: +```swift +public typealias BasicProgressReporter = ProgressReporter +public typealias FileProgressReporter = ProgressReporter +public typealias FileProgress = ProgressReporter.Progress +public typealias BasicProgress = ProgressReporter.Progress +``` + +### Introduce an `Observable` adapter for `ProgressReporter` +We thought about introducing a clearer separation of responsibility between the reporting and observing of a `ProgressReporter`, because progress reporting is often done by the framework, and the caller of a certain method of a framework would merely observe the `ProgressReporter` within the framework. This will deter observers from accidentally mutating values of a framework's `ProgressReporter`. + +However, this means that `ProgressReporter` needs to be passed into the `Observable` adapter to make an instance `ObservableProgressReporter`, which can then be passed into `ProgressView()` later. We decided that this is too much overhead for developers to use for the benefit of avoiding observers from mutating values of `ProgressReporter`. + +### Introduce Support for Cancellation, Pausing, and Resuming of `ProgressReporter` +Foundation's `Progress` provides support for cancelling, pausing and resuming an ongoing operation tracked by an instance of `Progress`, and propagates these actions down to all of its children. We decided to not introduce support for this behavior as there is support in cancelling a `Task` via `Task.cancel()` in Swift structured concurrency. The absence of support for cancellation, pausing and resuming in `ProgressReporter` helps to clarify the scope of responsibility of this API, which is to report progress, instead of owning a task and performing actions on it. + +### Move `totalCount` and `completedCount` properties to `ProgressProperties` protocol +We considered moving the `totalCount` and `completedCount` properties from `ProgressReporter` to `ProgressProperties` to allow developers the flexibility to set the Type of `totalCount` and `completedCount`. This would allow developers to set the Type to `Int`, `UInt128`, `Int64`, etc. While this flexibility may be desirable for allowing developers to determine what Type they need, most developers may not be concerned with the Type, and some Types may not pair well with the calculations that need be done within `ProgressReporter`. This flexibility may also lead to developer errors that cannot be handled by `ProgressReporter` such as having negative integers in `totalCount`, or assigning more than available units to create `ProgressReporter.Progress`. Having `totalCount` and `completedCount` as `Int` in `ProgressReporter` reduces programming errors and simplifies the process of using `ProgressReporter` to report progress. + +### Introduce `totalCount` and `completedCount` properties as `UInt64` +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 `FileProgressProperties`, which developers can set and display within `localizedDescription`. + +### Store Foundation's `Progress` in TaskLocal Storage +This would allow a `Progress` object to be stored in Swift `TaskLocal` storage. This allows the implicit model of building a progress tree to be used from Swift Concurrency asynchronous contexts. In this solution, getting the current `Progress` and adding a child `Progress` is done by first reading from TaskLocal storage when called from a Swift Concurrency context. This method was found to be not preferable as we would like to encourage the usage of the explicit model of Progress Reporting, in which we do not depend on an implicit TaskLocal storage and have methods that report progress to explicitly accepts a `Progress` object as a parameter. + +### Add Convenience Method to Foundation's `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: + +```swift +extension Progress { + public func makeChild(pendingUnitCount: Int64) -> Progress { + let child = Progress() + addChild(child, withPendingUnitCount: pendingUnitCount) + return child + } +} +``` +This method would mean that we are altering the usage pattern of pre-existing `Progress` API, which may introduce more confusions to developers in their efforts to move from non-async functions to async functions. + +## Acknowledgements +Thanks to [Tony Parker](https://github.com/parkera) and [Tina Liu](https://github.com/itingliu) for constant feedback and guidance throughout to help shape this API and proposal. I would also like to thank [Jeremy Schonfeld](https://github.com/jmschonfeld), [Cassie Jones](https://github.com/porglezomp), [Konrad Malawski](https://github.com/ktoso), [Philippe Hausler](https://github.com/phausler), Julia Vashchenko for valuable feedback on this proposal and its previous versions. From f3e5fde7d62bd9428ad4a4e6bf17c901648e3cc0 Mon Sep 17 00:00:00 2001 From: Chloe Yeo Date: Wed, 26 Feb 2025 10:04:27 -0800 Subject: [PATCH 02/29] reword future directions section + fix typo --- Proposals/NNNN-progress-reporter.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/NNNN-progress-reporter.md index 32efb4fb2..294c7b728 100644 --- a/Proposals/NNNN-progress-reporter.md +++ b/Proposals/NNNN-progress-reporter.md @@ -30,7 +30,7 @@ * [`Progress` \(Parent\) \- `ProgressReporter` \(Child\)](#progress-parent---progressreporter-child) * [Impact on Existing Code](#impact-on-existing-code) * [Future Directions](#future-directions) - * [ProgressView Overload in SwiftUI](#progressview-overload-in-swiftui) + * [Additional Overloads to APIs within UI Frameworks](#additional-overloads-to-apis-within-ui- frameworks) * [Distributed ProgressReporter](#distributed-progressreporter) * [Alternatives Considered](#alternatives-considered) * [Alternative Names](#alternative-names) @@ -99,7 +99,7 @@ let vegetables = ["spinach", "carrots", "celeries"] public func makeSalad() async { let progress = Progress(totalUnitCount: 2) - let choppingProgress = Progress() + let subprogress = Progress() progress.addChild(subprogress, withPendingUnitCount: 1) await chopFruits(progress: subprogress) @@ -912,9 +912,8 @@ There should be no impact on existing code, as this is an additive change. However, this new progress reporting API, `ProgressReporter`, which is compatible with Swift's async/await style concurrency, will be favored over the existing `Progress` API going forward. Depending on how widespread the adoption of `ProgressReporter` is, we may consider deprecating the existing `Progress` API. ## Future Directions - -### `ProgressView` Overload in SwiftUI -To enable the usage of `ProgressReporter` for app development, an overload for SwiftUI's `ProgressView` will be added. SwiftUI's `ProgressView` currently can be intitialized using a `Double` or the existing `Progress` instance. Adding support to allow for use of `ProgressView` with `ProgressReporter` will enable adoption of `ProgressReporter` for app developers who wish to take advantage of `ProgressReporter` to do relatively extensive progress reporting and show progress on the User Interface. +### Additional Overloads to APIs within UI Frameworks +To enable the usage of `ProgressReporter` for app development, we can add overloads to APIs within UI frameworks that has previously worked with `Progress`, such as `ProgressView` in SwiftUI. Adding support to existing progress-related APIs within UI Frameworks will enable adoption of `ProgressReporter` for app developers who wish to do extensive progress reporting and show progress on the User Interface using `ProgressReporter`. ### Distributed `ProgressReporter` To enable inter-process progress reporting, we would like to introduce distributed `ProgressReporter` in the future, which would functionally be similar to how Foundation's `Progress` mechanism for reporting progress across processes. From 6c98f1d1c11bf43df805afcc1f1fb6e56abfc328 Mon Sep 17 00:00:00 2001 From: Chloe Yeo Date: Wed, 26 Feb 2025 10:08:18 -0800 Subject: [PATCH 03/29] fix spacing --- Proposals/NNNN-progress-reporter.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/NNNN-progress-reporter.md index 294c7b728..5830bff8a 100644 --- a/Proposals/NNNN-progress-reporter.md +++ b/Proposals/NNNN-progress-reporter.md @@ -912,6 +912,7 @@ There should be no impact on existing code, as this is an additive change. However, this new progress reporting API, `ProgressReporter`, which is compatible with Swift's async/await style concurrency, will be favored over the existing `Progress` API going forward. Depending on how widespread the adoption of `ProgressReporter` is, we may consider deprecating the existing `Progress` API. ## Future Directions + ### Additional Overloads to APIs within UI Frameworks To enable the usage of `ProgressReporter` for app development, we can add overloads to APIs within UI frameworks that has previously worked with `Progress`, such as `ProgressView` in SwiftUI. Adding support to existing progress-related APIs within UI Frameworks will enable adoption of `ProgressReporter` for app developers who wish to do extensive progress reporting and show progress on the User Interface using `ProgressReporter`. From 4b15850bb1bafef9549d345322832878faa6d698 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 26 Feb 2025 23:46:36 -0800 Subject: [PATCH 04/29] refactor LocalizedDescriptionOptions --- Proposals/NNNN-progress-reporter.md | 47 +++++++++++++---------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/NNNN-progress-reporter.md index 5830bff8a..0c40fd769 100644 --- a/Proposals/NNNN-progress-reporter.md +++ b/Proposals/NNNN-progress-reporter.md @@ -568,24 +568,22 @@ public struct BasicProgressProperties : ProgressProperties { /// Returns `self`. This is because there are no properties in `BasicProgressProperties`. /// - Parameter children: An array of children of the same `Type`. /// - Returns: `self` - public func reduce(children: [Self]) -> Self { - return self - } + public func reduce(children: [Self]) -> Self /// A struct containing all options to choose in specifying how localized description should be generated. public struct LocalizedDescriptionOptions: Sendable, Hashable, Equatable { /// Option to include formatted `fractionCompleted` in localized description. /// Example: 20% completed. - /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. + /// - Parameter style: A `FloatingPointFormatStyle.Percent` used to format `fractionCompleted`. /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - public static func percentage(_ style: FloatingPointFormatStyle.Percent?) -> LocalizedDescriptionOptions + public static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent()) -> LocalizedDescriptionOptions /// Option to include formatted `completedCount` / `totalCount` in localized description. /// Example: 5 of 10 - /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. + /// - Parameter style: An `IntegerFormatStyle` instance used to format `completedCount` and `totalCount`. /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - public static func count(_ style: IntegerFormatStyle?) -> LocalizedDescriptionOptions + public static func count(format style: IntegerFormatStyle = IntegerFormatStyle()) -> LocalizedDescriptionOptions } /// Returns a `LocalizedStringResource` based on options provided. @@ -605,8 +603,8 @@ public struct BasicProgressProperties : ProgressProperties { #### `FileProgressProperties` -```swift -/// A custom `ProgressProperties` to incorporate additional properties such as `totalFileCount` to +```swift +/// A custom `ProgressProperties` to incorporate additional properties such as `totalFileCount` to /// ProgressReporter, which itself tracks only general properties such as `totalCount`. @available(FoundationPreview 6.2, *) public struct FileProgressProperties : ProgressProperties { @@ -652,43 +650,39 @@ public struct FileProgressProperties : ProgressProperties { /// Option to include formatted `fractionCompleted` in localized description. /// Example: 20% completed. - /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. + /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance used to format `fractionCompleted`. /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - public static func percentage(_ style: FloatingPointFormatStyle.Percent?) -> LocalizedDescriptionOptions { - return LocalizedDescriptionOptions("Percentage", formatPercentage: style, formatCount: nil) - } + public static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent()) -> LocalizedDescriptionOptions /// Option to include formatted `completedCount` / `totalCount` in localized description. /// Example: 5 of 10 - /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. + /// - Parameter style: An `IntegerFormatStyle` instance used to format `completedCount` and `totalCount`. /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - public static func count(_ style: IntegerFormatStyle?) -> LocalizedDescriptionOptions { - return LocalizedDescriptionOptions("Percentage", formatPercentage: nil, formatCount: style) - } + public static func count(format style: IntegerFormatStyle = IntegerFormatStyle()) -> LocalizedDescriptionOptions /// Option to include `completedFileCount` / `totalFileCount` in localized description. /// Example: 1 of 5 files - /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedFileCount` and `totalFileCount`. + /// - Parameter style: An `IntegerFormatStyle` instance used to format `completedFileCount` and `totalFileCount`. /// - Returns: A `LocalizedStringResource` for formatted `completedFileCount` / `totalFileCount`. - public static func files(_ style: IntegerFormatStyle?) -> LocalizedDescriptionOptions + public static func fileCount(format style: IntegerFormatStyle = IntegerFormatStyle()) -> LocalizedDescriptionOptions /// Option to include formatted `completedByteCount` / `totalByteCount` in localized description. /// Example: Zero kB of 123.5 MB - /// - Parameter style: A `ByteCountFormatStyle` instance that should be used to format `completedByteCount` and `totalByteCount`. If this is `nil`, it defaults to `ByteCountFormatStyle(style: .file, allowedUnits: .all, spellsOutZero: true, includesActualByteCount: false, locale: .autoupdatingCurrent)`. + /// - Parameter style: A `ByteCountFormatStyle` instance used to format `completedByteCount` and `totalByteCount`. /// - Returns: A `LocalizedDescriptionOption` for formatted `completedByteCount` / `totalByteCount`. - public static func bytes(_ style: ByteCountFormatStyle?) -> LocalizedDescriptionOptions + public static func byteCount(format style: ByteCountFormatStyle = ByteCountFormatStyle()) -> LocalizedDescriptionOptions /// Option to include formatted `throughput` (bytes per second) in localized description. /// Example: 10 MB/s - /// - Parameter style: `ByteCountFormatStyle` instance used to format `completedByteCount` and `totalByteCount`. If this is `nil`, it defaults to `ByteCountFormatStyle(style: .file, allowedUnits: .all, spellsOutZero: true, includesActualByteCount: false, locale: .autoupdatingCurrent)`. + /// - Parameter style: A `ByteCountFormatStyle` instance used to format `throughput`. /// - Returns: A `LocalizedDescriptionOption` for formatted `throughput`. - public static func throughput(_ style: ByteCountFormatStyle?) -> LocalizedDescriptionOptions + public static func throughput(format style: ByteCountFormatStyle = ByteCountFormatStyle()) -> LocalizedDescriptionOptions - /// Option to include formatted `estimatedTimeRemaining` in localized description. + /// Option to include `estimatedTimeRemaining` in localized description. /// Example: 5 minutes remaining - /// - Parameter style: `Duration.UnitsFormatStyle` instance used to format `estimatedTimeRemaining`, which is of `Duration` Type. If this is `nil`, it defaults to `Duration.UnitsFormatStyle(allowedUnits: Set(arrayLiteral: .hours, .minutes), width: .wide)`. + /// - Parameter style: `Duration.UnitsFormatStyle` instance used to format `estimatedTimeRemaining`, which is of `Duration` Type. /// - Returns: A `LocalizedDescriptionOption` for formatted `estimatedTimeRemaining`. - public static func estimatedTimeRemaining(_ style: Duration.UnitsFormatStyle?) -> LocalizedDescriptionOptions + public static func estimatedTimeRemaining(format style: Duration.UnitsFormatStyle = Duration.UnitsFormatStyle(allowedUnits: Set(arrayLiteral: .hours, .minutes), width: .wide)) -> LocalizedDescriptionOptions } /// Returns a custom `LocalizedStringResource` for file-related `ProgressReporter` of `FileProgressProperties` based on the selected `LocalizedDescriptionOptions`. @@ -992,4 +986,5 @@ extension Progress { This method would mean that we are altering the usage pattern of pre-existing `Progress` API, which may introduce more confusions to developers in their efforts to move from non-async functions to async functions. ## Acknowledgements + Thanks to [Tony Parker](https://github.com/parkera) and [Tina Liu](https://github.com/itingliu) for constant feedback and guidance throughout to help shape this API and proposal. I would also like to thank [Jeremy Schonfeld](https://github.com/jmschonfeld), [Cassie Jones](https://github.com/porglezomp), [Konrad Malawski](https://github.com/ktoso), [Philippe Hausler](https://github.com/phausler), Julia Vashchenko for valuable feedback on this proposal and its previous versions. From 3e59ff1bfa2161e0463afd775f7c83eb6fe5ad74 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 27 Feb 2025 00:01:34 -0800 Subject: [PATCH 05/29] edit future directions --- Proposals/NNNN-progress-reporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/NNNN-progress-reporter.md index 0c40fd769..5d2cd4d0a 100644 --- a/Proposals/NNNN-progress-reporter.md +++ b/Proposals/NNNN-progress-reporter.md @@ -908,7 +908,7 @@ However, this new progress reporting API, `ProgressReporter`, which is compatibl ## Future Directions ### Additional Overloads to APIs within UI Frameworks -To enable the usage of `ProgressReporter` for app development, we can add overloads to APIs within UI frameworks that has previously worked with `Progress`, such as `ProgressView` in SwiftUI. Adding support to existing progress-related APIs within UI Frameworks will enable adoption of `ProgressReporter` for app developers who wish to do extensive progress reporting and show progress on the User Interface using `ProgressReporter`. +To enable the usage of `ProgressReporter` for app development, we can add overloads to APIs within UI frameworks that has been using Foundation's `Progress`, such as `ProgressView` in SwiftUI. Adding support to existing progress-related APIs within UI Frameworks will enable adoption of `ProgressReporter` for app developers who wish to do extensive progress reporting and show progress on the User Interface using `ProgressReporter`. ### Distributed `ProgressReporter` To enable inter-process progress reporting, we would like to introduce distributed `ProgressReporter` in the future, which would functionally be similar to how Foundation's `Progress` mechanism for reporting progress across processes. From 5ed834e817f41600208ebf0359d6d6faab97a984 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 3 Apr 2025 13:02:39 -0700 Subject: [PATCH 06/29] update ProgressReporter Pitch to V2 --- Proposals/NNNN-progress-reporter.md | 919 ++++++++++++---------------- 1 file changed, 398 insertions(+), 521 deletions(-) diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/NNNN-progress-reporter.md index 5d2cd4d0a..588eda585 100644 --- a/Proposals/NNNN-progress-reporter.md +++ b/Proposals/NNNN-progress-reporter.md @@ -3,46 +3,55 @@ * Proposal: SF-NNNN * Author(s): [Chloe Yeo](https://github.com/chloe-yeo) * Review Manager: TBD -* Status: **Draft** +* Status: **Pitch** ## Revision history * **v1** Initial version +* **v2** Major Updates: + - Replaced generics with `@dynamicMemberLookup` to account for additional metadata + - Replaced localized description methods with `ProgressReporter.FormatStyle` and `ProgressReporter.FileFormatStyle` + - Replaced top level `totalCount` to be get-only and only settable via `withProperties` closure + - Added the ability for `completedCount` to be settable via `withProperties` closure + - Omitted checking of `Task.cancellation` in `complete(count:)` method ## Table of Contents * [Introduction](#introduction) * [Motivation](#motivation) * [Proposed Solution and Example](#proposed-solution-and-example) - * [Reporting Progress with Identical Properties](#reporting-progress-with-identical-properties) - * [Reporting Progress with Distinct Properties](#reporting-progress-with-distinct-properties) - * [Reporting Progress with Task Cancellation](#reporting-progress-with-task-cancellation) + * [Reporting Progress (General Operations)](#reporting-progress-general-operations) + * [Reporting Progress (File-Related Operations)](#reporting-progress-file\-related-operations) * [Advantages of using `ProgressReporter.Progress` as Currency Type](#advantages-of-using-progresssreporterprogress-as-currency-type) - * [Interoperability with Foundation\'s `Progress`](#interoperability-with-foundations-progress) + * [Interoperability with Existing `Progress`](#interoperability-with-existing-progress) * [Detailed Design](#detailed-design) - * [`ProgressProperties`](#progressproperties) - * [`BasicProgressProperties`](#basicprogressproperties) - * [`FileProgressProperties`](#fileprogressproperties) * [`ProgressReporter`](#progressreporter) + * [`ProgressReporter.Properties`](#progressreporterproperties) * [`ProgressReporter.Progress`](#progressreporterprogress) - * [Interoperability with Foundation's `Progress`](#methods-for-interoperability-with-foundations-progress) + * [`ProgressReporter.FormatStyle`](#progressreporterformatstyle) + * [`ProgressReporter.FileFormatStyle`](#progressreporterfileformatstyle) + * [Interoperability with Existing `Progress`](#methods-for-interoperability-with-existing-progress) * [`ProgressReporter` \(Parent\) \- `Progress` \(Child\)](#progressreporter-parent---progress-child) * [`Progress` \(Parent\) \- `ProgressReporter` \(Child\)](#progress-parent---progressreporter-child) * [Impact on Existing Code](#impact-on-existing-code) * [Future Directions](#future-directions) - * [Additional Overloads to APIs within UI Frameworks](#additional-overloads-to-apis-within-ui- frameworks) + * [ProgressView Overloads](#progressview-overloads) * [Distributed ProgressReporter](#distributed-progressreporter) + * [Enhanced `FormatStyle`](#enhanced-formatstyle) * [Alternatives Considered](#alternatives-considered) * [Alternative Names](#alternative-names) * [Introduce `ProgressReporter` to Swift standard library](#introduce-progressreporter-to-swift-standard-library) - * [Implement `ProgressReporter` as an actor](#implement-progressreporter-as-an-actor) - * [Implement `ProgressReporter` as a protocol](#implement-progressreporter-as-a-protocol) - * [Introduce an Observable adapter for `ProgressReporter`](#introduce-an-observable-adapter-for-progressreporter) - * [Introduce Support for Cancellation, Pausing, Resuming of `ProgressReporter`](#introduce-support-for-cancellation-pausing-and-resuming-of-progressreporter) - * [Move totalCount and completedCount properties to `ProgressProperties` protocol](#move-totalcount-and-completedcount-properties-to-progressproperties-protocol) - * [Introduce totalCount and completedCount properties as UInt64](#introduce-totalcount-and-completedcount-properties-as-uint64) - * [Store Foundation\'s `Progress` in TaskLocal Storage](#store-foundations-progress-in-tasklocal-storage) - * [Add Convenience Method to Foundation\'s `Progress` for Easier Instantiation of Child Progress](#add-convenience-method-to-foundations-progress-for-easier-instantiation-of-child-progress) + * [Implement `ProgressReporter` as a Generic Class](#implement-progressreporter-as-a-generic-class) + * [Implement `ProgressReporter` as an Actor](#implement-progressreporter-as-an-actor) + * [Implement `ProgressReporter` as a Protocol](#implement-progressreporter-as-a-protocol) + * [Introduce an `Observable` Adapter for `ProgressReporter`](#introduce-an-observable-adapter-for-progressreporter) + * [Introduce Method to Generate Localized Description](#introduce-method-to-generate-localized-description) + * [Introduce Explicit Support for Cancellation, Pausing, Resuming of `ProgressReporter`](#introduce-explicit-support-for-cancellation-pausing-and-resuming-of-progressreporter) + * [Check Task Cancellation within `complete(count:)` Method](#check-task-cancellation-within-completecount-method) + * [Introduce totalCount and completedCount Properties as UInt64](#introduce-totalcount-and-completedcount-properties-as-uint64) + * [Store Existing `Progress` in TaskLocal Storage](#store-existing-progress-in-tasklocal-storage) + * [Add Convenience Method to Existing `Progress` for Easier Instantiation of Child Progress](#add-convenience-method-to-existing-progress-for-easier-instantiation-of-child-progress) + * [Allow for Assignment of `ProgressReporter` to Multiple Progress Reporter Trees](#allow-for-assignment-of-progressreporter-to-multiple-progress-reporter-trees) * [Acknowledgements](#acknowledgements) ## Introduction @@ -55,9 +64,9 @@ This proposal aims to introduce an efficient, easy-to-use, less error-prone Prog ## Motivation -A progress reporting mechanism that is compatible with Swift's async/await style concurrency would be to pass a `Progress` instance as a parameter to functions or methods that report progress. The current recommended usage pattern of Foundation's `Progress`, as outlined in [Apple Developer Documentation](https://developer.apple.com/documentation/foundation/progress), does not fit well with async/await style concurrency. Typically, a function that aims to report progress to its callers will first return an instance of Foundation's `Progress`. The returned instance is then added as a child to a parent `Progress` instance. +A progress reporting mechanism that is compatible with Swift's async/await style concurrency would be to pass a `Progress` instance as a parameter to functions or methods that report progress. The current recommended usage pattern of the existing `Progress`, as outlined in [Apple Developer Documentation](https://developer.apple.com/documentation/foundation/progress), does not fit well with async/await style concurrency. Typically, a function that aims to report progress to its callers will first return an instance of the existing `Progress`. The returned instance is then added as a child to a parent `Progress` instance. -In the following example, the function `chopFruits(completionHandler:)` reports progress to its caller, `makeSalad()`. +In the following example, the function `chopFruits(completionHandler:)` reports progress to its caller, `makeSalad()`. ```swift public func makeSalad() { @@ -75,8 +84,8 @@ public func makeSalad() { public func chopFruits(completionHandler: @escaping (Result) -> Void) -> Progress {} ``` - -When we update this function to use async/await, the previous pattern no longer composes as expected: +When we +update this function to use async/await, the previous pattern no longer composes as expected: ```swift public func makeSalad() async { @@ -88,9 +97,9 @@ public func makeSalad() async { public func chopFruits() async -> Progress {} ``` -The previous pattern of "returning" the `Progress` instance no longer composes as expected because we are forced to await the `chopFruits()` call. We could _then_ return the `Progress` instance. However, the `Progress` instance that gets returned already has its `completedUnitCount` equal to `totalUnitCount`. This defeats its purpose of showing incremental progress as the code runs to completion within the method. +The previous pattern of "returning" the `Progress` instance no longer composes as expected because we are forced to await the `chopFruits()` call before returning the `Progress` instance. However, the `Progress` instance that gets returned already has its `completedUnitCount` equal to `totalUnitCount`. This defeats its purpose of showing incremental progress as the code runs to completion within the method. -Additionally, while it may be possible to reuse Foundation's `Progress` to report progress in an `async` function by passing `Progress` as an argument to the function reporting progress, it is more error-prone, as shown below: +Additionally, while it may be possible to reuse the existing `Progress` to report progress in an `async` function by passing `Progress` as an argument to the function reporting progress, it is more error-prone, as shown below: ```swift let fruits = ["apple", "orange", "melon"] @@ -99,12 +108,12 @@ let vegetables = ["spinach", "carrots", "celeries"] public func makeSalad() async { let progress = Progress(totalUnitCount: 2) - let subprogress = Progress() + let choppingProgress = Progress() progress.addChild(subprogress, withPendingUnitCount: 1) await chopFruits(progress: subprogress) - await chopVegetables(progress: subprogress) // Author's Mistake, same subprogress was passed! + await chopVegetables(progress: subprogress) // Author's mistake: same subprogress was reused! } public func chopFruits(progress: Progress) async { @@ -116,260 +125,171 @@ public func chopFruits(progress: Progress) async { } public func chopVegetables(progress: Progress) async { - progress.totalUnitCount = Int64(vegetables.count) // Author's Mistake, overrides progress made in chopFruits as same subprogress was passed! + progress.totalUnitCount = Int64(vegetables.count) // Author's mistake: overwriting progress made in `chopFruits` on the same `progress` instance! for vegetable in vegetables { await chopItem(vegetable) progress.completedUnitCount += 1 } } -public func chopItem(_ item: String) async {} +public func chopItem(_ item: Ingredient) async {} ``` -The existing `Progress` in Foundation was not designed in a way that enforces the usage of `Progress` instance as a function parameter to report progress. Without a strong rule about who creates the `Progress` and who consumes it, it is easy to end up in a situation where the `Progress` is used more than once. This results in nondeterministic behavior when developers may accidentally overcomplete or override a `Progress` instance. +The existing `Progress` was not designed in a way that enforces the usage of `Progress` instance as a function parameter to report progress. Without a strong rule about who creates the `Progress` and who consumes it, it is easy to end up in a situation where the `Progress` is used more than once. This results in nondeterministic behavior when developers may accidentally overcomplete or overwrite a `Progress` instance. -In contrast, the introduction of a new progress reporting mechanism following the new `ProgressReporter` type would enforce safer practices of progress reporting via a strong rule of what should be passed as parameter and what should be used to report progress. +We introduce a new progress reporting mechanism following the new `ProgressReporter` type. This type encourages safer practices of progress reporting, separating what to be passed as parameter from what to be used to report progress. This proposal outlines the use of `ProgressReporter` as reporters of progress and `~Copyable` `ProgressReporter.Progress` as parameters passed to progress reporting methods. ## Proposed solution and example -Before proceeding further with this proposal, it is important to keep in mind the type aliases introduced with this API. The examples outlined in the following sections will utilize type aliases as follows: - -```swift -public typealias BasicProgressReporter = ProgressReporter -public typealias FileProgressReporter = ProgressReporter - -public typealias FileProgress = ProgressReporter.Progress -public typealias BasicProgress = ProgressReporter.Progress -``` - -### Reporting Progress With Identical Properties +### Reporting Progress (General Operations) To begin, let's create a class called `MakeSalad` that reports progress made on a salad while it is being made. ```swift struct Fruit { - let name: String - - init(_ fruit: String) { - self.name = fruit - } - - func chop() async {} + func chop() async { ... } } struct Dressing { - let name: String - - init (_ dressing: String) { - self.name = dressing - } - - func pour() async {} + func pour() async { ... } } public class MakeSalad { - let overall: BasicProgressReporter + let overall: ProgressReporter let fruits: [Fruit] let dressings: [Dressing] public init() { - overall = BasicProgressReporter(totalCount: 100) - fruits = [Fruit("apple"), Fruit("banana"), Fruit("cherry")] - dressings = [Dressing("mayo"), Dressing("mustard"), Dressing("ketchup")] + overall = ProgressReporter(totalCount: 100) + ... } } ``` -In order to report progress on subparts of making a salad, such as `chopFruits` and `mixDressings`, we can instantitate subprogresses by passing an instance of `ProgressReporter.Progress` to each subpart. Each `ProgressReporter.Progress` passed into the subparts then have to be consumed to initialize an instance of `ProgressReporter`. This is done by calling `reporter(totalCount:)` on `ProgressReporter.Progress`. These child progresses will automatically contribute to the `overall` progress reporter within the class, due to established parent-children relationships between `overall` and the reporters of subparts. This can be done as follows: +In order to report progress on subparts of making a salad, such as `chopFruits` and `mixDressings`, we pass an instance of `ProgressReporter.Progress` to each subpart. Each `ProgressReporter.Progress` passed into the subparts then has to be consumed to initialize an instance of `ProgressReporter`. This is done by calling `reporter(totalCount:)` on `ProgressReporter.Progress`. These `ProgressReporter`s of subparts will contribute to the `overall` progress reporter within the class, due to established parent-children relationships between `overall` and the reporters of subparts. This can be done as follows: ```swift extension MakeSalad { public func start() async -> String { - // Gets a BasicProgress instance with 70 portioned count from `overall` - let fruitsProgress = overall.assign(count: 70) - await chopFruits(progress: fruitsProgress) + // Assign a `ProgressReporter.Progress` instance with 70 portioned count from `overall` to `chopFruits` method + await chopFruits(progress: overall.assign(count: 70)) - // Gets a BasicProgress instance with 30 portioned count from `overall` - let dressingsProgress = overall.assign(count: 30) - await mixDressings(progress: dressingsProgress) + // Assign a `ProgressReporter.Progress` instance with 30 portioned count from `overall` to `mixDressings` method + await mixDressings(progress: overall.assign(count: 30)) return "Salad is ready!" } - private func chopFruits(progress: consuming BasicProgress?) async { - // Initializes a progress reporter to report progress on chopping fruits - // with passed-in progress parameter - let choppingReporter = progress?.reporter(totalCount: fruits.count) + private func chopFruits(progress: consuming ProgressReporter.Progress) async { + // Initialize a progress reporter to report progress on chopping fruits + // with passed-in progress parameter + let choppingReporter = progress.reporter(totalCount: fruits.count) for fruit in fruits { await fruit.chop() - choppingReporter?.complete(count: 1) + choppingReporter.complete(count: 1) } } - private func mixDressings(progress: consuming BasicProgress?) async { - // Initializes a progress reporter to report progress on mixing dressing - // with passed-in progress parameter - let dressingReporter = progress?.reporter(totalCount: dressings.count) + private func mixDressings(progress: consuming ProgressReporter.Progress) async { + // Initialize a progress reporter to report progress on mixing dressing + // with passed-in progress parameter + let dressingReporter = progress.reporter(totalCount: dressings.count) for dressing in dressings { await dressing.pour() - dressingReporter?.complete(count: 1) + dressingReporter.complete(count: 1) } } } ``` -### Reporting Progress With Distinct Properties +### Reporting Progress (File-Related Operations) -`ProgressReporter`, which is a generic class, allows developers to define their own type of `ProgressProperties`, and report progress with additional metadata or properties. We propose adding `BasicProgressProperties` for essential use cases and a `FileProgressProperties` for reporting progress on file-related operations. Developers can create progress trees in which all instances of `ProgressReporter` are of the same kind, or a mix of `ProgressReporter` instances with different `ProgressProperties`. +With the use of @dynamicMemberLookup attribute, `ProgressReporter` is able to access properties that are not explicitly defined in the class. This means that developers are able to define additional properties on the class specific to the operations they are reporting progress on. For instance, we pre-define additional file-related properties on `ProgressReporter` by extending `ProgressReporter` for use cases of reporting progress on file operations. -In this section, we will show an example of how progress reporting with different kinds of `ProgressReporter` can be done. To report progress on both making salad and downloading images, developers can use both `BasicProgressReporter` and `FileProgressReporter` which are children reporters to an overall `BasicProgressReporter`, as follows: +>Note: The mechanisms of how extending `ProgressReporter` to include additional properties will be shown in the Detailed Design section of the proposal. -```swift -struct Fruit { - let name: String - - init(_ fruit: String) { - self.name = fruit - } - - func chop() async {} -} +In this section, we will show an example of how we report progress with additional file-related properties: -struct Image { +To begin, let's create a class `ImageProcessor` that first downloads images, then applies a filter on all the images downloaded. We can track the progress of this operation in two subparts, so we begin by instantiating an overall `ProgressReporter` with a total count of 2. - let bytes: Int +```swift +struct Image { + let bytes: UInt64 - init(bytes: Int) { - self.bytes = bytes - } + func download() async { ... } - func read() async {} + func applyFilter() async { ... } } -class Multitask { - - let overall: BasicProgressReporter - // These are stored in this class to keep track of - // additional properties of reporters of chopFruits and downloadImages - var chopFruits: BasicProgressReporter? - var downloadImages: FileProgressReporter? - let fruits: [Fruit] +final class ImageProcessor: Sendable { + + let overall: ProgressReporter let images: [Image] init() { - overall = BasicProgressReporter(totalCount: 100) - fruits = [Fruit("apple"), Fruit("banana"), Fruit("cherry")] + overall = ProgressReporter(totalCount: 2) images = [Image(bytes: 1000), Image(bytes: 2000), Image(bytes: 3000)] } - - func chopFruitsAndDownloadImages() async { - // Gets a BasicProgress instance with 50 portioned count from `overall` - let chopProgress = overall.assign(count: 50) - await chop(progress: chopProgress) - - // Gets a FileProgress instance with 50 portioned count from `overall` - let downloadProgress = overall.assign(count: 50, kind: FileProgressProperties.self) - await download(progress: downloadProgress) - } } ``` -Here's how you can compose two different kinds of progress into the same tree, with `overall` being the top-level `ProgressReporter`. `overall` has two children — `chopFruits` of Type `BasicProgressReporter`, and `downloadImages` of Type `FileProgressReporter`. You can report progress to both `chopFruits` and `downloadImages` as follows: +In order to report progress on the subparts of downloading images and applying filter onto the images, we assign 1 count of `overall`'s `totalCount` to each subpart. -```swift -extension Multitask { +The subpart of downloading images contains information such as `totalByteCount` that we want to report along with the properties directly defined on a `ProgressReporter`. While `totalByteCount` is not directly defined on the `ProgressReporter` class, we can still set the property `totalByteCount` via the `withProperties` closure because this property can be discovered at runtime via the `@dynamicMemberLookup` attribute. - func chop(progress: consuming BasicProgress?) async { - // Initializes a BasicProgressReporter to report progress on chopping fruits - // with passed-in `progress` parameter - chopReporter = progress?.reporter(totalCount: fruits.count) - for fruit in fruits { - await fruit.chop() - chopReporter?.complete(count: 1) - } - } +The subpart of applying filter does not contain additional file-related information, so we report progress on this subpart as usual. - func download(progress: consuming FileProgress?) async { - // Initializes a FileProgressReporter to report progress on file downloads - // with passed-in `progress` parameter - downloadReporter = progress?.reporter(totalCount: images.count, properties: FileProgressProperties()) - for image in images { - if let reporter = downloadReporter { - // Passes in a FileProgress instance with 1 portioned count from `downloadImages` - await read(image, progress: reporter.assign(count: 1)) - } - } - } - - func read(_ image: Image, progress: consuming FileProgress?) async { - // Instantiates a FileProgressProperties with known properties - // to be passed into `reporter(totalCount: properties:)` - let fileProperties = FileProgressProperties(totalFileCount: 1, totalByteCount: image.bytes) - - // Initializes a FileProgressReporter with passed-in `progress` parameter - let readFile = progress?.reporter(totalCount: 1, properties: fileProperties) - - // Initializes other file-related properties of `readFile` that are only obtained later - readFile?.properties.throughput = calculateThroughput() - readFile?.properties.estimatedTimeRemaining = calculateEstimatedTimeRemaining() - - await image.read() - - // Updates file-related properties of `readFile` - readFile?.properties.completedFileCount += 1 - readFile?.properties.completedByteCount += image.bytes +```swift +extension ImageProcessor { + func downloadImagesFromDiskAndApplyFilter() async { + // Assign a `ProgressReporter.Progress` instance with 1 portioned count from `overall` to `downloadImagesFromDisk` + await downloadImagesFromDisk(progress: overall.assign(count: 1)) - // Completes `readFile` entirely - readFile?.complete(count: 1) + // Assign a `ProgressReporter.Progress` instance with 1 portioned count from `overall` to `applyFilterToImages` + await applyFilterToImages(progress: overall.assign(count: 1)) } -} -``` - -### Reporting Progress with Task Cancellation - -A `ProgressReporter` running in a `Task` can respond to the cancellation of the `Task`. In structured concurrency, cancellation of the parent task results in the cancellation of all child tasks. Mirroring this behavior, a `ProgressReporter` running in a parent `Task` that is cancelled will have its children instances of `ProgressReporter` cancelled as well. - -Cancellation in the context of `ProgressReporter` means that any subsequent calls to `complete(count:)` after a `ProgressReporter` is cancelled results in a no-op. Trying to update 'cancelled' `ProgressReporter` and its children will no longer increase `completedCount`, thus no further forward progress will be made. - -While the code can continue running, calls to `complete(count:)` from a `Task` that is cancelled will result in a no-op, as follows: - -```swift -let fruits = ["apple", "banana", "cherry"] -let overall = BasicProgressReporter(totalCount: fruits.count) - -func chopFruits(_ fruits: [String]) async -> [String] { - await withTaskGroup { group in - - // Concurrently chop fruits - for fruit in fruits { - group.addTask { - await FoodProcessor.chopFruit(fruit: fruit, progress: overall.assign(count: 1)) - } - if fruit == "banana" { - group.cancelAll() - } - } + + func downloadImagesFromDisk(progress: consuming ProgressReporter.Progress) async { + // Initialize a progress reporter to report progress on downloading images + // with passed-in progress parameter + let reporter = progress.reporter(totalCount: images.count) - // Collect chopped fruits - var choppedFruits: [String] = [] - for await choppedFruit in group { - choppedFruits.append(choppedFruit) + // Initialize file-related properties on the reporter + reporter.withProperties { properties in + properties.totalFileCount = images.count + properties.completedFileCount = 0 + properties.totalByteCount = images.map { $0.bytes }.reduce(0, +) + properties.completedByteCount = 0 } - return choppedFruits + for image in images { + await image.download() + reporter.complete(count: 1) + // Update each file-related property + reporter.withProperties { properties in + if let completedFileCount = properties.completedFileCount, let completedByteCount = properties.completedByteCount { + properties.completedFileCount = completedFileCount + 1 + properties.completedByteCount = completedByteCount + image.bytes + } else { + properties.completedFileCount = 1 + properties.completedByteCount = image.bytes + } + } + } } -} - -class FoodProcessor { - static func chopFruit(fruit: String, progress: consuming BasicProgress?) async -> String { - let progressReporter = progress?.reporter(totalCount: 1) - ... // expensive async work here - progressReporter?.complete(count: 1) // This becomes a no-op if the Task is cancelled - return "Chopped \(fruit)" + + func applyFilterToImages(progress: consuming ProgressReporter.Progress) async { + // Initializes a progress reporter to report progress on applying filter + // with passed-in progress parameter + let reporter = progress.reporter(totalCount: images.count) + for image in images { + await image.applyFilter() + reporter.complete(count: 1) + } } } ``` @@ -383,14 +303,14 @@ The advantages of `ProgressReporter` mainly derive from the use of `ProgressRepo `ProgressReporter.Progress` should be used as the currency to be passed into progress-reporting methods, within which a child `ProgressReporter` instance that constitutes a portion of its parent's total units is created via a call to `reporter(totalCount:)`, as follows: ```swift -func testCorrectlyReportToSubprogressAfterInstantiatingReporter() async { - let overall = BasicProgressReporter(totalCount: 2) +func correctlyReportToSubprogressAfterInstantiatingReporter() async { + let overall = ProgressReporter(totalCount: 2) await subTask(progress: overall.assign(count: 1)) } -func subTask(progress: consuming BasicProgress?) async { +func subTask(progress: consuming ProgressReporter.Progress) async { let count = 10 - let progressReporter = progress?.reporter(totalCount: count) // returns an instance of ProgressReporter that can be used to report subprogress + let progressReporter = progress.reporter(totalCount: count) // returns an instance of ProgressReporter that can be used to report subprogress for _ in 1...count { progressReporter?.complete(count: 1) // reports progress as usual } @@ -401,17 +321,17 @@ While developers may accidentally make the mistake of trying to report progress Each time before progress reporting happens, there needs to be a call to `reporter(totalCount:)`, which returns a `ProgressReporter` instance, before calling `complete(count:)` on the returned `ProgressReporter`. -The following faulty example shows how reporting progress directly to `ProgressReporter.Progress` without initializing it will be cause a compiler error. Developers will always need to instantiate a `ProgressReporter` from `ProgresReporter.Progress` before reporting progress. +The following faulty example shows how reporting progress directly to `ProgressReporter.Progress` without initializing it will be cause a compiler error. Developers will always need to instantiate a `ProgressReporter` from `ProgresReporter.Progress` before reporting progress. ```swift -func testIncorrectlyReportToSubprogressWithoutInstantiatingReporter() async { - let overall = BasicProgressReporter(totalCount: 2) +func incorrectlyReportToSubprogressWithoutInstantiatingReporter() async { + let overall = ProgressReporter(totalCount: 2) await subTask(progress: overall.assign(count: 1)) } -func subTask(progress: consuming BasicProgress?) async { - // COMPILER ERROR: Value of type 'BasicProgress' (aka 'ProgressReporter.Progress') has no member 'complete' - progress?.complete(count: 1) +func subTask(progress: consuming ProgressReporter.Progress) async { + // COMPILER ERROR: Value of type 'ProgressReporter.Progress' has no member 'complete' + progress.complete(count: 1) } ``` @@ -420,8 +340,8 @@ func subTask(progress: consuming BasicProgress?) async { Developers should create only one `ProgressReporter.Progress` for a corresponding to-be-instantiated `ProgressReporter` instance, as follows: ```swift -func testCorrectlyConsumingSubprogress() { - let overall = BasicProgressReporter(totalCount: 2) +func correctlyConsumingSubprogress() { + let overall = ProgressReporter(totalCount: 2) let progressOne = overall.assign(count: 1) // create one ProgressReporter.Progress let reporterOne = progressOne.reporter(totalCount: 10) // initialize ProgressReporter instance with 10 units @@ -436,10 +356,10 @@ It is impossible for developers to accidentally consume `ProgressReporter.Progre The `reporter(totalCount:)` method, which **consumes** the `ProgressReporter.Progress`, can only be called once on each `ProgressReporter.Progress` instance. If there are more than one attempts to call `reporter(totalCount:)` on the same instance of `ProgressReporter.Progress`, the code will not compile due to the `~Copyable` nature of `ProgressReporter.Progress`. ```swift -func testIncorrectlyConsumingSubprogress() { - let overall = BasicProgressReporter(totalCount: 2) +func incorrectlyConsumingSubprogress() { + let overall = ProgressReporter(totalCount: 2) - let progressOne = overall.assign(count: 1) // create one BasicProgress + let progressOne = overall.assign(count: 1) // create one ProgressReporter.Progress let reporterOne = progressOne.reporter(totalCount: 10) // initialize ProgressReporter instance with 10 units // COMPILER ERROR: 'progressOne' consumed more than once @@ -447,14 +367,14 @@ func testIncorrectlyConsumingSubprogress() { } ``` -### Interoperability with Foundation's `Progress` +### Interoperability with Existing `Progress` -In both cases below, the propagation of progress of subparts to a root progress should work the same ways Foundation's `Progress` and `ProgressReporter` work. +In both cases below, the propagation of progress of subparts to a root progress should work the same ways the existing `Progress` and `ProgressReporter` work. -Consider two progress reporting methods, one which utilizes Foundation's `Progress`, and another using `ProgressReporter`: +Consider two progress reporting methods, one which utilizes the existing `Progress`, and another using `ProgressReporter`: ```swift -// Framework code: Function reporting progress with Foundation's `Progress` +// Framework code: Function reporting progress with the existing `Progress` func doSomethingWithProgress() -> Progress { let p = Progress.discreteProgress(totalUnitCount: 2) Task.detached { @@ -467,12 +387,12 @@ func doSomethingWithProgress() -> Progress { } // Framework code: Function reporting progress with `ProgressReporter` -func doSomethingWithReporter(progress: consuming BasicProgress?) async -> Int { - let reporter = progress?.reporter(totalCount: 2) +func doSomethingWithReporter(progress: consuming ProgressReporter.Progress) async -> Int { + let reporter = progress.reporter(totalCount: 2) //do something - reporter?.complete(count: 1) + reporter.complete(count: 1) //do something - reporter?.complete(count: 1) + reporter.complete(count: 1) } ``` @@ -482,8 +402,8 @@ The choice of naming the interop method as `assign(count: to:)` is to keep the s ```swift // Developer code -func testProgressReporterParentProgressChildInterop() async { - let overall = BasicProgressReporter(totalCount: 2) // Top-level `ProgressReporter` +func reporterParentProgressChildInterop() async { + let overall = ProgressReporter(totalCount: 2) // Top-level `ProgressReporter` // Assigning 1 unit of overall's `totalCount` to `ProgressReporter.Progress` let progressOne = overall.assign(count: 1) @@ -491,9 +411,9 @@ func testProgressReporterParentProgressChildInterop() async { let result = await doSomethingWithReporter(progress: progressOne) - // Getting a Foundation's `Progress` from method reporting progress + // Getting a `Progress` from method reporting progress let progressTwo = doSomethingWithProgress() - // Assigning 1 unit of overall's `totalCount` to Foundation's `Progress` + // Assigning 1 unit of overall's `totalCount` to the existing `Progress` overall.assign(count: 1, to: progressTwo) } ``` @@ -504,16 +424,16 @@ The choice of naming the interop method as `makeChild(withPendingUnitCount: kind ```swift // Developer code -func testProgressParentProgressReporterChildInterop() { - let overall = Progress(totalUnitCount: 2) // Top-level Foundation's `Progress` +func progressParentReporterChildInterop() { + let overall = Progress(totalUnitCount: 2) // Top-level `Progress` - // Getting a Foundation's `Progress` from method reporting progress + // Getting a `Progress` from method reporting progress let progressOne = doSomethingWithProgress() // Add Foundation's `Progress` as a child which takes up 1 unit of overall's `totalUnitCount` overall.addChild(progressOne, withPendingUnitCount: 1) // Getting a `ProgressReporter.Progress` which takes up 1 unit of overall's `totalUnitCount` - let progressTwo = overall.makeChild(withPendingUnitCount: 1, kind: BasicProgressProperties.self) + let progressTwo = overall.makeChild(withPendingUnitCount: 1) // Passing `ProgressReporter.Progress` instance to method reporting progress doSomethingWithReporter(progress: progressTwo) } @@ -521,345 +441,272 @@ func testProgressParentProgressReporterChildInterop() { ## Detailed design -### `ProgressProperties` - -The `ProgressProperties` protocol allows you to specify additional properties of a `ProgressReporter` instance. You can create conforming types that contains additional properties on top of the required properties and methods, and these properties further customize `ProgressReporter` and provide more information in localized descriptions returned. +### `ProgressReporter` -In addition to specifying additional properties, the `ProgressProperties` protocol also allows you to specify `LocalizedDescriptionOptions` as options for localized descriptions of a `ProgressReporter` provided to observers. +`ProgressReporter` is an Observable and Sendable class that developers use to report progress. Specifically, an instance of `ProgressReporter` can be used to either track progress of a single task, or track progress of a tree of `ProgressReporter` instances. -```swift -/// `ProgressProperties` is a protocol that defines the requirement for any type of `ProgressReporter`. +```swift +/// An object that conveys ongoing progress to the user for a specified task. @available(FoundationPreview 6.2, *) -public protocol ProgressProperties : Hashable, Sendable { +@Observable public final class ProgressReporter : Sendable, Hashable, Equatable, CustomDebugStringConvertible { + + /// The total units of work. + public var totalCount: Int? { get } - /// Returns a new instance of Self which represents the aggregation of an array of Self. - /// - /// Default implementation returns Self as it assumes that there are no children. - /// - Parameter children: An array of Self to be aggregated. - /// - Returns: An instance of Self. - func reduce(children: [Self]) -> Self + /// The completed units of work. + /// If `self` is indeterminate, the value will be 0. + public var completedCount: Int { get } - /// A struct containing options for specifying localized description. - associatedtype LocalizedDescriptionOptions : Hashable, Equatable + /// 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 } - /// Returns a `LocalizedStringResource` for a `ProgressReporter` instance based on `LocalizedDescriptionOptions` specified. - /// - Parameters: - /// - progress: `ProgressReporter` instance to generate localized description for. - /// - options: A set of `LocalizedDescriptionOptions` to include in localized description. - /// - Returns: A `LocalizedStringResource`. - func localizedDescription(_ progress: ProgressReporter, _ options: Set) -> LocalizedStringResource -} -``` + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. + public var isIndeterminate: Bool { get } -There are two implemented conforming types of `ProgressProperties`, namely: -1. `BasicProgressProperties`: No additional properties -2. `FileProgressProperties`: Additional properties for progress on file-related operations + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. + public var isFinished: Bool { get } -#### `BasicProgressProperties` + /// A type that conveys additional task-specific information on progress. + public protocol Property { -```swift -/// A basic implementation of ProgressProperties that contains no additional properties. -@available(FoundationPreview 6.2, *) -public struct BasicProgressProperties : ProgressProperties { + associatedtype T : Sendable - /// Initializes an instance of `BasicProgressProperties`. - public init() - - /// Returns `self`. This is because there are no properties in `BasicProgressProperties`. - /// - Parameter children: An array of children of the same `Type`. - /// - Returns: `self` - public func reduce(children: [Self]) -> Self + /// Aggregates an array of `T` into a single value `T`. + /// - Parameter all: Array of `T` to be aggregated. + /// - Returns: A new instance of `T`. + static func reduce(_ all: [T]) -> T + } - /// A struct containing all options to choose in specifying how localized description should be generated. - public struct LocalizedDescriptionOptions: Sendable, Hashable, Equatable { - - /// Option to include formatted `fractionCompleted` in localized description. - /// Example: 20% completed. - /// - Parameter style: A `FloatingPointFormatStyle.Percent` used to format `fractionCompleted`. - /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - public static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent()) -> LocalizedDescriptionOptions + /// 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 } - /// Option to include formatted `completedCount` / `totalCount` in localized description. - /// Example: 5 of 10 - /// - Parameter style: An `IntegerFormatStyle` instance used to format `completedCount` and `totalCount`. - /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - public static func count(format style: IntegerFormatStyle = IntegerFormatStyle()) -> LocalizedDescriptionOptions + /// Returns a property value that a key path indicates. + public subscript

(dynamicMember key: KeyPath) -> P.T? where P : ProgressReporter.ProgressReporter.Property { get set } } - /// Returns a `LocalizedStringResource` based on options provided. + /// Initializes `self` with `totalCount`. /// - /// Examples of localized description that can be generated include: - /// 20% completed - /// 2 of 10 - /// 2 of 10 - 20% completed + /// If `totalCount` is set to `nil`, `self` is indeterminate. + /// - Parameter totalCount: Total units of work. + public convenience init(totalCount: Int?) + + /// Returns a `ProgressReporter.Progress` representing a portion of `self`which can be passed to any method that reports progress. /// - /// - Parameters: - /// - progress: `ProgressReporter` instance to generate localized description for. - /// - options: A set of `LocalizedDescriptionOptions` to specify information to be included in localized description. - /// - Returns: A `LocalizedStringResource`. - public func localizedDescription(_ progress: ProgressReporter, _ options: Set) -> LocalizedStringResource + /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `ProgressReporter.Progress`. + /// - Returns: A `ProgressReporter.Progress` instance. + public func assign(count portionOfParent: Int) -> Progress + + /// Increases `completedCount` by `count`. + /// - Parameter count: Units of work. + public func complete(count: Int) + + /// Accesses or mutates any properties that convey additional information about progress. + public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T +} + +/// Default implementation for `reduce` where T is `AdditiveArithmetic`. +@available(FoundationPreview 6.2, *) +extension ProgressReporter.Property where Self.T : AdditiveArithmetic { + /// Aggregates an array of `T` into a single value `T`. + /// + /// All `T` `AdditiveArithmetic` values are added together. + /// - Parameter all: Array of `T` to be aggregated. + /// - Returns: A new instance of `T`. + public static func reduce(_ all: [T]) -> T } ``` -#### `FileProgressProperties` +### `ProgressReporter.Properties` + +`ProgressReporter.Properties` is a struct that contains declarations of additional properties that are not defined directly on `ProgressReporter`, but discovered at runtime via `@dynamicMemberLookup`. These additional properties should be defined separately in `ProgressReporter` because neither are they used to drive forward progress like `totalCount` and `completedCount`, nor are they applicable in all cases of progress reporting. + +We pre-declare some of these additional properties that are commonly desired in use cases of progress reporting such as `totalFileCount` and `totalByteCount`. + +For developers that would like to report additional metadata or properties as they use `ProgressReporter` to report progress, they will need to add declarations of their additional properties into `ProgressReporter.Properties`, similar to how the pre-declared additional properties are declared. ```swift -/// A custom `ProgressProperties` to incorporate additional properties such as `totalFileCount` to -/// ProgressReporter, which itself tracks only general properties such as `totalCount`. @available(FoundationPreview 6.2, *) -public struct FileProgressProperties : ProgressProperties { +extension ProgressReporter { - /// Initializes an instance of `FileProgressProperties` with all fields as `nil` or defaults. - public init() - - /// Initializes an instance of `FileProgressProperties`. - /// - Parameters: - /// - totalFileCount: Total number of files. - /// - totalByteCount: Total number of bytes. - /// - completedFileCount: Completed number of files. - /// - completedByteCount: Completed number of bytes. - /// - throughput: Throughput in bytes per second. - /// - estimatedTimeRemaining: A `Duration` representing amount of time remaining to completion. - public init(totalFileCount: Int?, totalByteCount: UInt64?, completedFileCount: Int = 0, completedByteCount: UInt64 = 0, throughput: UInt64? = nil, estimatedTimeRemaining: Duration? = nil) + public struct Properties { - /// An Int representing total number of files. - public var totalFileCount: Int? + /// The total number of files. + public var totalFileCount: TotalFileCount.Type { get } - /// An Int representing completed number of files. - public var completedFileCount: Int + public struct TotalFileCount : Property { - /// A UInt64 representing total bytes. - public var totalByteCount: UInt64? + public typealias T = Int + } - /// A UInt64 representing completed bytes. - public var completedByteCount: UInt64 + /// The number of completed files. + public var completedFileCount: CompletedFileCount.Type { get } - /// A UInt64 representing throughput in bytes per second. - public var throughput: UInt64? + public struct CompletedFileCount : Property { - /// A Duration representing amount of time remaining in the processing of files. - public var estimatedTimeRemaining: Duration? + public typealias T = Int + } - /// Returns a new `FileProgressProperties` instance that is a result of aggregating an array of children`FileProgressProperties` instances. - /// - Parameter children: An Array of `FileProgressProperties` instances to be aggregated into a new `FileProgressProperties` instance. - /// - Returns: A `FileProgressProperties` instance. - public func reduce(children: [FileProgressProperties]) -> FileProgressProperties + /// The total number of bytes. + public var totalByteCount: TotalByteCount.Type { get } - /// A struct containing all options to choose in specifying how localized description should be generated. - public struct LocalizedDescriptionOptions: Sendable, Hashable, Equatable { - - /// Option to include formatted `fractionCompleted` in localized description. - /// Example: 20% completed. - /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance used to format `fractionCompleted`. - /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - public static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent()) -> LocalizedDescriptionOptions - - /// Option to include formatted `completedCount` / `totalCount` in localized description. - /// Example: 5 of 10 - /// - Parameter style: An `IntegerFormatStyle` instance used to format `completedCount` and `totalCount`. - /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - public static func count(format style: IntegerFormatStyle = IntegerFormatStyle()) -> LocalizedDescriptionOptions - - /// Option to include `completedFileCount` / `totalFileCount` in localized description. - /// Example: 1 of 5 files - /// - Parameter style: An `IntegerFormatStyle` instance used to format `completedFileCount` and `totalFileCount`. - /// - Returns: A `LocalizedStringResource` for formatted `completedFileCount` / `totalFileCount`. - public static func fileCount(format style: IntegerFormatStyle = IntegerFormatStyle()) -> LocalizedDescriptionOptions - - /// Option to include formatted `completedByteCount` / `totalByteCount` in localized description. - /// Example: Zero kB of 123.5 MB - /// - Parameter style: A `ByteCountFormatStyle` instance used to format `completedByteCount` and `totalByteCount`. - /// - Returns: A `LocalizedDescriptionOption` for formatted `completedByteCount` / `totalByteCount`. - public static func byteCount(format style: ByteCountFormatStyle = ByteCountFormatStyle()) -> LocalizedDescriptionOptions - - /// Option to include formatted `throughput` (bytes per second) in localized description. - /// Example: 10 MB/s - /// - Parameter style: A `ByteCountFormatStyle` instance used to format `throughput`. - /// - Returns: A `LocalizedDescriptionOption` for formatted `throughput`. - public static func throughput(format style: ByteCountFormatStyle = ByteCountFormatStyle()) -> LocalizedDescriptionOptions - - /// Option to include `estimatedTimeRemaining` in localized description. - /// Example: 5 minutes remaining - /// - Parameter style: `Duration.UnitsFormatStyle` instance used to format `estimatedTimeRemaining`, which is of `Duration` Type. - /// - Returns: A `LocalizedDescriptionOption` for formatted `estimatedTimeRemaining`. - public static func estimatedTimeRemaining(format style: Duration.UnitsFormatStyle = Duration.UnitsFormatStyle(allowedUnits: Set(arrayLiteral: .hours, .minutes), width: .wide)) -> LocalizedDescriptionOptions - } + public struct TotalByteCount : Property { - /// Returns a custom `LocalizedStringResource` for file-related `ProgressReporter` of `FileProgressProperties` based on the selected `LocalizedDescriptionOptions`. - /// Examples of localized description that can be generated include: - /// 20% completed - /// 5 of 10 files - /// 2 minutes remaining - /// 2 of 10 - 20% completed - /// - /// - Parameters: - /// - progress: `ProgressReporter` instance to generate localized description for. - /// - options: A set of `LocalizedDescriptionOptions` to specify information to be included in localized description. - /// - Returns: A `LocalizedStringResource`. - public func localizedDescription(_ progress: ProgressReporter, _ options: Set) -> LocalizedStringResource + public typealias T = UInt64 + } + + /// The number of completed bytes. + public var completedByteCount: CompletedByteCount.Type { get } + + public struct CompletedByteCount : Property { + + public typealias T = UInt64 + } + + /// The throughput, in bytes per second. + public var throughput: Throughput.Type { get } + + public struct Throughput : Property { + + public typealias T = UInt64 + } + + /// The amount of time remaining in the processing of files. + public var estimatedTimeRemaining: EstimatedTimeRemaining.Type { get } + + public struct EstimatedTimeRemaining : Property { + + public typealias T = Duration + } + } } ``` -### `ProgressReporter` +### `ProgressReporter.Progress` -`ProgressReporter` serves as a generic interface for users to instantiate progress reporting, which can be characterized further using custom `Properties` created by developers. An instance of `ProgressReporter` can be used to either track progress of a single task, or track progress of a tree of `ProgressReporter` instances. +An instance of `ProgressReporter.Progress` is returned from a call to `ProgressReporter`'s `assign(count:)`. `ProgressReporter.Progress` acts as an intermediary instance that you pass into functions that report progress. Additionally, callers should convert `ProgressReporter.Progress` to `ProgressReporter` before starting to report progress with it by calling `reporter(totalCount:)`. -```swift -/// Typealiases for ProgressReporter -public typealias BasicProgressReporter = ProgressReporter -public typealias FileProgressReporter = ProgressReporter +```swift +@available(FoundationPreview 6.2, *) +extension ProgressReporter { -/// Typealiases for ProgressReporter.Progress -public typealias BasicProgress = ProgressReporter.Progress -public typealias FileProgress = ProgressReporter.Progress + public struct Progress : ~Copyable, Sendable { + + /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. + /// - Parameter totalCount: Total count of returned child `ProgressReporter` instance. + /// - Returns: A `ProgressReporter` instance. + public consuming func reporter(totalCount: Int?) -> ProgressReporter + } +} +``` -/// ProgressReporter is a Sendable class used to report progress in a tree structure. -@available(FoundationPreview 6.2, *) -@Observable public final class ProgressReporter : Sendable, Hashable, Equatable { +### `ProgressReporter.FormatStyle` - /// Represents total count of work to be done. - /// Setting this to `nil` means that `self` is indeterminate, - /// and developers should later set this value to an `Int` value before using `self` to report progress for `fractionCompleted` to be non-zero. - public var totalCount: Int? { get set } +`ProgressReporter.FormatStyle` is used to configure the formatting of `ProgressReporter` into localized descriptions. You can specify which option to format `ProgressReporter` with, and call the `format(_:)` method to get a localized string containing information that you have specified when initializing a `ProgressReporter.FormatStyle`. - /// Represents completed count of work. - /// If `self` is indeterminate, returns 0. - public var completedCount: Int { get } +```swift +@available(FoundationPreview 6.2, *) +extension ProgressReporter { - /// Represents the fraction completed of the current instance, - /// taking into account the fraction completed in its children instances if children are present. - /// If `self` is indeterminate, returns `0.0`. - public var fractionCompleted: Double { get } + public struct FormatStyle : Codable, Equatable, Hashable { - /// Represents whether work is completed, - /// returns `true` if completedCount >= totalCount. - public var isFinished: Bool { get } + public struct Option : Codable, Hashable, Equatable { - /// Represents whether `totalCount` is initialized to an `Int`, - /// returns `true` only if `totalCount == nil`. - public var isIndeterminate: Bool { get } + /// Option specifying `fractionCompleted`. + /// + /// For example, 20% completed. + /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. + /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. + public static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent()) -> Option - /// Access point to additional properties such as `fileTotalCount` - /// declared within struct of custom type `ProgressProperties`. - public var properties: Properties { get set } + /// Option specifying `completedCount` / `totalCount`. + /// + /// For example, 5 of 10. + /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. + /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. + public static func count(format style: IntegerFormatStyle = IntegerFormatStyle()) -> Option + } + + public var locale: Locale - /// Initializes `self` with `totalCount` and `properties`. - /// If `totalCount` is set to `nil`, `self` is indeterminate. - /// - /// - Parameters: - /// - totalCount: Total count of work. - /// - properties: An instance of`ProgressProperties`. - public convenience init(totalCount: Int?) + public init(_ option: Option, locale: Locale = .autoupdatingCurrent) + } +} - /// Increases completedCount by `count`. - /// - /// This operation becomes a no-op if Task from which `self` gets created is cancelled. - /// - Parameter count: Number of units that `completedCount` should be incremented by. - public func complete(count: Int) +@available(FoundationPreview 6.2, *) +extension ProgressReporter.FormatStyle : FormatStyle { - /// Returns a `ProgressReporter.Progress` which can be passed to any method that reports progress. - /// - /// Delegates a portion of `self`'s `totalCount` to a to-be-initialized child `ProgressReporter` instance. - /// - /// - Parameter count: Count of units delegated to a child instance of `ProgressReporter` - /// which may be instantiated by calling `reporter(totalCount:)`. - /// - Parameter kind: `ProgressProperties` of child instance of `ProgressReporter`. - /// - Returns: A `ProgressReporter.Progress` instance. - public func assign(count: Int, kind: AssignedProperties.Type = AssignedProperties.self) -> ProgressReporter.Progress - - /// Overload for `assign(count: kind:)` for cases where - /// `ProgressReporter.Progress` has the same properties of `ProgressReporter`. - public func assign(count: Int) -> ProgressReporter.Progress - - /// Returns a `LocalizedStringResource` for `self`. - /// - /// Examples of localized descriptions that can be generated for `BasicProgressReporter` include: - /// 5 of 10 - /// 50% completed - /// 5 of 10 - 50% completed - /// - /// Examples of localized descriptions that can be generated for `FileProgressReporter` include: - /// 2 of 10 files - /// Zero kB of 123.5 MB - /// 2 minutes remaining - /// - /// - Parameter options: A set of `LocalizedDescriptionOptions` to include in localized description. - /// - Returns: A `LocalizedStringResource` based on `options`. - public func localizedDescription(including options: Set) -> LocalizedStringResource + public func locale(_ locale: Locale) -> ProgressReporter.FormatStyle + + public func format(_ reporter: ProgressReporter) -> String } +``` + +To provide convenience methods for formatting `ProgressReporter`, we also provide the `formatted(_:)` method that developers can call on any `ProgressReporter`. +```swift @available(FoundationPreview 6.2, *) -extension ProgressReporter where Properties == BasicProgressProperties { - - /// Initializes `self` with `totalCount` and `properties`. - /// If `totalCount` is set to `nil`, `self` is indeterminate. - /// - /// - Parameters: - /// - totalCount: Total count of work. - /// - properties: An instance of `BasicProgressProperties`. - public convenience init(totalCount: Int?, properties: BasicProgressProperties = BasicProgressProperties()) +extension ProgressReporter { + + public func formatted(_ style: F) -> F.FormatOutput where F : FormatStyle, F.FormatInput == ProgressReporter } @available(FoundationPreview 6.2, *) -extension ProgressReporter where Properties == FileProgressProperties { - - /// Initializes `self` with `totalCount` and `properties`. - /// If `totalCount` is set to `nil`, `self` is indeterminate. - /// - /// - Parameters: - /// - totalCount: Total count of work. - /// - properties: An instance of `FileProgressProperties`. - public convenience init(totalCount: Int?, properties: FileProgressProperties = FileProgressProperties()) +extension FormatStyle where Self == ProgressReporter.FormatStyle { + + public static func fractionCompleted(format: FloatingPointFormatStyle.Percent) -> Self + + public static func count(format: IntegerFormatStyle) -> Self } ``` -### `ProgressReporter.Progress` +### `ProgressReporter.FileFormatStyle` -An instance of `ProgressReporter.Progress` is returned from a call to `ProgressReporter`'s `assign(count: kind:)`. `ProgressReporter.Progress` acts as an intermediary instance that you pass into functions that report progress. Additionally, callers should convert `ProgressReporter.Progress` to `ProgressReporter` before starting to report progress with it by calling `reporter(totalCount:)`. +The custom format style for additional file-related properties are also implemented as follows: ```swift @available(FoundationPreview 6.2, *) extension ProgressReporter { - /// ProgressReporter.Progress is a nested ~Copyable struct used to establish parent-child relationship between two instances of ProgressReporter. - /// - /// ProgressReporter.Progress is returned from a call to `assign(count:)` by a parent ProgressReporter. - /// A child ProgressReporter is then returned by calling`reporter(totalCount:)` on a ProgressReporter.Progress. - public struct Progress : ~Copyable, Sendable { + public struct FileFormatStyle : Codable, Equatable, Hashable { - /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. - /// - Parameters: - /// - totalCount: Total count of returned child `ProgressReporter` instance. - /// - properties: An instance of conforming type of`ProgressProperties`. - /// - Returns: A `ProgressReporter` instance. - public consuming func reporter(totalCount: Int?, properties: Properties) -> ProgressReporter + public struct Options : Codable, Equatable, Hashable { + + /// Option specifying all file-related properties. + public static var file: Option { get } + } + + public var locale: Locale + + public init(_ option: Options, locale: Locale = .autoupdatingCurrent) } } @available(FoundationPreview 6.2, *) -extension ProgressReporter.Progress where Properties == BasicProgressProperties { +extension ProgressReporter.FileFormatStyle : FormatStyle { - /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. - /// - Parameters: - /// - totalCount: Total count of returned child `ProgressReporter` instance. - /// - properties: An instance of `BasicProgressProperties`. - /// - Returns: A `ProgressReporter` instance. - public consuming func reporter(totalCount: Int?, properties: BasicProgressProperties = BasicProgressProperties()) -> ProgressReporter + public func locale(_ locale: Locale) -> ProgressReporter.FileFormatStyle + + public func format(_ reporter: ProgressReporter) -> String } @available(FoundationPreview 6.2, *) -extension ProgressReporter.Progress where Properties == FileProgressProperties { +extension FormatStyle where Self == ProgressReporter.FileFormatStyle { - /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. - /// - Parameters: - /// - totalCount: Total count of returned child `ProgressReporter` instance. - /// - properties: An instance of `FileProgressProperties`. - /// - Returns: A `ProgressReporter` instance. - public consuming func reporter(totalCount: Int?, properties: FileProgressProperties = FileProgressProperties()) -> ProgressReporter + public static var file: Self { get } } ``` -### Methods for Interoperability with Foundation's `Progress` +### Methods for Interoperability with Existing `Progress` To allow frameworks which may have dependencies on the pre-existing progress-reporting protocol to adopt this new progress-reporting protocol, either as a recipient of a child `Progress` instance that needs to be added to its `ProgressReporter` tree, or as a provider of `ProgressReporter` that may later be added to another framework's `Progress` tree, there needs to be additional support for ensuring that progress trees can be composed with in two cases: 1. A `ProgressReporter` instance has to parent a `Progress` child @@ -872,7 +719,7 @@ To add an instance of `Progress` as a child to an instance of `ProgressReporter` ```swift @available(FoundationPreview 6.2, *) extension ProgressReporter { - // Adds a Foundation's `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. + // Adds a `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. /// - Parameters: /// - count: Number of units delegated from `self`'s `totalCount`. /// - progress: `Progress` which receives the delegated `count`. @@ -882,7 +729,7 @@ extension ProgressReporter { #### Progress (Parent) - ProgressReporter (Child) -To add an instance of `ProgressReporter` as a child to an instance of Foundation's `Progress`, the `Progress` instance calls `makeChild(count:kind:)` to get a `ProgressReporter.Progress` instance that can be passed as a parameter to a function that reports progress. The `Progress` instance will track the `ProgressReporter` instance as a child, just like any of its `Progress` children. +To add an instance of `ProgressReporter` as a child to an instance of the existing `Progress`, the `Progress` instance calls `makeChild(count:kind:)` to get a `ProgressReporter.Progress` instance that can be passed as a parameter to a function that reports progress. The `Progress` instance will track the `ProgressReporter` instance as a child, just like any of its `Progress` children. ```swift @available(FoundationPreview 6.2, *) @@ -894,8 +741,8 @@ extension Progress { /// /// - Parameter count: Number of units delegated to a child instance of `ProgressReporter` /// which may be instantiated by `ProgressReporter.Progress` later when `reporter(totalCount:)` is called. - /// - Returns: A `ProgressReporter.Progress` instance. - public func makeChild(withPendingUnitCount count: Int, kind: Kind.Type = Kind.self) -> ProgressReporter.Progress + /// - Returns: A `ProgressReporter.Progress` instance. + public func makeChild(withPendingUnitCount count: Int) -> ProgressReporter.Progress } ``` @@ -908,15 +755,18 @@ However, this new progress reporting API, `ProgressReporter`, which is compatibl ## Future Directions ### Additional Overloads to APIs within UI Frameworks -To enable the usage of `ProgressReporter` for app development, we can add overloads to APIs within UI frameworks that has been using Foundation's `Progress`, such as `ProgressView` in SwiftUI. Adding support to existing progress-related APIs within UI Frameworks will enable adoption of `ProgressReporter` for app developers who wish to do extensive progress reporting and show progress on the User Interface using `ProgressReporter`. +To enable wider adoption of `ProgressReporter`, we can add overloads to APIs within UI frameworks that has been using Foundation's `Progress`, such as `ProgressView` in SwiftUI. Adding support to existing progress-related APIs within UI Frameworks will enable adoption of `ProgressReporter` for app developers who wish to do extensive progress reporting and show progress on the User Interface using `ProgressReporter`. ### Distributed `ProgressReporter` To enable inter-process progress reporting, we would like to introduce distributed `ProgressReporter` in the future, which would functionally be similar to how Foundation's `Progress` mechanism for reporting progress across processes. +### Enhanced `FormatStyle` +To enable more customization of `ProgressReporter`, we would like to introduce more options in `ProgressReporter`'s `FormatStyle`. + ## Alternatives considered ### Alternative Names -As Foundation's `Progress` already exists, we had to come up with a name other than `Progress` for this API, but one that still conveys the progress-reporting functionality of this API. Some of the names we have considered are as follows: +As the existing `Progress` already exists, we had to come up with a name other than `Progress` for this API, but one that still conveys the progress-reporting functionality of this API. Some of the names we have considered are as follows: 1. Alternative to `ProgressReporter` - `AsyncProgress` @@ -928,50 +778,56 @@ We decided to proceed with the name `ProgressReporter` because prefixing an API - `ProgressReporter.Child` - `ProgressReporter.Token` -While the names `Link`, `Child`, and `Token` may appeal to the fact that this is a type that is separate from the `ProgressReporter` itself and should only be used as a function parameter and to be consumed immediately to kickstart progress reporting, it is ambiguous because developers may not immedidately figure out its function from just the name itself. `Progress` is an intuitive name because developers will instinctively think of the term `Progress` when they want to adopt `ProgressReporting`. - -3. Alternative to `ProgressProperties` protocol - - `ProgressKind` - -While the name `ProgressKind` conveys the message that this is a protocol that developers should conform to when they want to create a different kind of `ProgressReporter`, the protocol mainly functions as a blueprint for developers to add additional properties to the existing properties such as `totalCount` and `completedCount` within `ProgressReporter`, so `ProgressProperties` reads more appropriately here. +While the names `Link`, `Child`, and `Token` may appeal to the fact that this is a type that is separate from the `ProgressReporter` itself and should only be used as a function parameter and to be consumed immediately to kickstart progress reporting, it is ambiguous because developers may not immedidately figure out its function from just the name itself. `Progress` is an intuitive name because developers will instinctively think of the term `Progress` when they want to adopt `ProgressReporting`. ### Introduce `ProgressReporter` to Swift standard library In consideration for making `ProgressReporter` a lightweight API for server-side developers to use without importing the entire `Foundation` framework, we considered either introducing `ProgressReporter` in a standalone module, or including `ProgressReporter` in existing Swift standard library modules such as `Observation` or `Concurrency`. However, given the fact that `ProgressReporter` has dependencies in `Observation` and `Concurrency` modules, and that the goal is to eventually support progress reporting over XPC connections, `Foundation` framework is the most ideal place to host the `ProgressReporter` as it is the central framework for APIs that provide core functionalities when these functionalities are not provided by Swift standard library and its modules. +### Implement `ProgressReporter` as a Generic Class +In Version 1 of this proposal, we proposed implementing `ProgressReporter` as a generic class, which has a type parameter `Properties`, which conforms to the protocol `ProgressProperties`. In this case, the API reads as `ProgressReporter`. This was implemented as such to account for additional properties required in different use cases of progress reporting. For instance, `FileProgressProperties` is a type of `ProgressProperties` that holds references to properties related to file operations such as `totalByteCount` and `totalFileCount`. The `ProgressReporter` class itself will then have a `properties` property, which holds a reference to its `Properties` struct, in order to access additional properties via dot syntax, which would read as `reporter.properties.totalByteCount`. In this implementation, the typealiases introduced are as follows: + + ```swift + public typealias BasicProgressReporter = ProgressReporter + public typealias FileProgressReporter = ProgressReporter + public typealias FileProgress = ProgressReporter.Progress + public typealias BasicProgress = ProgressReporter.Progress + ``` + +However, while this provides flexibility for developers to create any custom types of `ProgressReporter`, some issues that arise include the additional properties of a child `ProgressReporter` being inaccessible by its parent `ProgressReporter` if they were not of the same type. For instance, if the child is a `FileProgressReporter` while the parent is a `BasicProgressReporter`, the parent does not have access to the child's `FileProgressProperties` because it only has reference to its own `BasicProgressProperties`. This means that developers would not be able to display additional file-related properties reported by its child in its localized descriptions without an extra step of adding a layer of children to parent different types of children in the progress reporter tree. + +We decided to replace the generic class implementation with `@dynamicMemberLookup`, making the `ProgressReporter` class non-generic, and instead relies on `@dynamicMemberLookup` to access additional properties that developers may want to use in progress reporting. This allows `ProgressReporter` to all be of the same `Type`, and at the same time retains the benefits of being able to report progress with additional properties such as `totalByteCount` and `totalFileCount`. With all progress reporters in a tree being the same type, a top-level `ProgressReporter` can access any additional properties reported by its children `ProgressReporter` without much trouble as compared to if `ProgressReporter` were to be a generic class. + ### Implement `ProgressReporter` as an actor We considered implementing `ProgressReporter` as we want to maintain this API as a reference type that is safe to use in concurrent environments. However, if `ProgressReporter` were to be implemented, `ProgressReporter` will not be able to conform to `Observable` because actor-based keypaths do not exist as of now. Ensuring that `ProgressReporter` is `Observable` is important to us, as we want to ensure that `ProgressReporter` works well with UI components in SwiftUI. ### Implement `ProgressReporter` as a protocol -In consideration of making the surface of the API simpler without the use of generics, we considered implementing `ProgressReporter` as a protocol, and provide implementations for specialized `ProgressReporter` classes that conform to the protocol, namely `BasicProgress`(`ProgressReporter` for progress reporting with only simple `count`) and `FileProgress` (`ProgressReporter` for progress reporting with file-related additional properties such as `totalFileCount`). This had the benefit of developers having to initialize a `ProgressReporter` instance with `BasicProgress(totalCount: 10)` instead of `ProgressReporter(totalCount: 10)`. +In consideration of making the surface of the API simpler without the use of generics, we considered implementing `ProgressReporter` as a protocol, and provide implementations for specialized `ProgressReporter` classes that conform to the protocol, namely `BasicProgress`(`ProgressReporter` for progress reporting with only simple `count`) and `FileProgress` (`ProgressReporter` for progress reporting with file-related additional properties such as `totalFileCount`). This had the benefit of developers having to initialize a `ProgressReporter` instance with `BasicProgress(totalCount: 10)` instead of `ProgressReporter(totalCount: 10)`. However, one of the downside of this is that every time a developer wants to create a `ProgressReporter` that contains additional properties that are tailored to their use case, they would have to write an entire class that conforms to the `ProgressReporter` protocol from scratch, including the calculations of `fractionCompleted` for `ProgressReporter` trees. Additionally, the `~Copyable` struct nested within the `ProgressReporter` class that should be used as function parameter passed to functions that report progress will have to be included in the `ProgressReporter` protocol as an `associatedtype` that is `~Copyable`. However, the Swift compiler currently cannot suppress 'Copyable' requirement of an associated type and developers will need to consciously work around this. These create a lot of overload for developers wishing to report progress with additional metadata beyond what we provide in `BasicProgress` and `FileProgress` in this case. -We decided to proceed with implementing `ProgressReporter` as a generic class to lessen the overhead for developers in customizing metadata for `ProgressReporter`, and at the same time introduce typealiases that simplify the API surface as follows: -```swift -public typealias BasicProgressReporter = ProgressReporter -public typealias FileProgressReporter = ProgressReporter -public typealias FileProgress = ProgressReporter.Progress -public typealias BasicProgress = ProgressReporter.Progress -``` - ### Introduce an `Observable` adapter for `ProgressReporter` We thought about introducing a clearer separation of responsibility between the reporting and observing of a `ProgressReporter`, because progress reporting is often done by the framework, and the caller of a certain method of a framework would merely observe the `ProgressReporter` within the framework. This will deter observers from accidentally mutating values of a framework's `ProgressReporter`. However, this means that `ProgressReporter` needs to be passed into the `Observable` adapter to make an instance `ObservableProgressReporter`, which can then be passed into `ProgressView()` later. We decided that this is too much overhead for developers to use for the benefit of avoiding observers from mutating values of `ProgressReporter`. -### Introduce Support for Cancellation, Pausing, and Resuming of `ProgressReporter` -Foundation's `Progress` provides support for cancelling, pausing and resuming an ongoing operation tracked by an instance of `Progress`, and propagates these actions down to all of its children. We decided to not introduce support for this behavior as there is support in cancelling a `Task` via `Task.cancel()` in Swift structured concurrency. The absence of support for cancellation, pausing and resuming in `ProgressReporter` helps to clarify the scope of responsibility of this API, which is to report progress, instead of owning a task and performing actions on it. +### Introduce Method to Generate Localized Description +We considered introducing a `localizedDescription(including:)` method, which returns a `LocalizedStringResource` for observers to get custom format descriptions for `ProgressReporter`. In contrast, using a `FormatStyle` aligns more closely with Swift's API, and has more flexibility for developers to add custom `FormatStyle` to display localized descriptions for additional properties they may want to declare and use. + +### Introduce Explicit Support for Cancellation, Pausing, and Resuming of `ProgressReporter` +The existing `Progress` provides support for cancelling, pausing and resuming an ongoing operation tracked by an instance of `Progress`, and propagates these actions down to all of its children. We decided to not introduce support for this behavior as there is support in cancelling a `Task` via `Task.cancel()` in Swift structured concurrency. The absence of support for cancellation, pausing and resuming in `ProgressReporter` helps to clarify the scope of responsibility of this API, which is to report progress, instead of owning a task and performing actions on it. -### Move `totalCount` and `completedCount` properties to `ProgressProperties` protocol -We considered moving the `totalCount` and `completedCount` properties from `ProgressReporter` to `ProgressProperties` to allow developers the flexibility to set the Type of `totalCount` and `completedCount`. This would allow developers to set the Type to `Int`, `UInt128`, `Int64`, etc. While this flexibility may be desirable for allowing developers to determine what Type they need, most developers may not be concerned with the Type, and some Types may not pair well with the calculations that need be done within `ProgressReporter`. This flexibility may also lead to developer errors that cannot be handled by `ProgressReporter` such as having negative integers in `totalCount`, or assigning more than available units to create `ProgressReporter.Progress`. Having `totalCount` and `completedCount` as `Int` in `ProgressReporter` reduces programming errors and simplifies the process of using `ProgressReporter` to report progress. +### Check Task Cancellation within `complete(count:)` Method +We considered adding a `Task.isCancelled` check in the `complete(count:)` method so that calls to `complete(count:)` from a `Task` that is cancelled becomes a no-op. This means that once a Task is cancelled, calls to `complete(count:)` from within the task does not make any further incremental progress. + +We decided to remove this check to transfer the responsibility back to the developer to not report progress further from within a cancelled task. Typically, developers complete some expensive async work and subsequently updates the `completedCount` of a `ProgressReporter` by calling `complete(count:)`. Checking `Task.isCancelled` means that we take care of the cancellation by not making any further incremental progress, but developers are still responsible for the making sure that they do not execute any of the expensive async work. Removing the `Task.isCancelled` check from `complete(count:)` helps to make clear that developers will be responsible for both canceling any expensive async work and any further update to `completedCount` of `ProgressReporter` when `Task.isCancelled` returns `true`. ### Introduce `totalCount` and `completedCount` properties as `UInt64` 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 `FileProgressProperties`, which developers can set and display within `localizedDescription`. -### Store Foundation's `Progress` in TaskLocal Storage +### Store Existing `Progress` in TaskLocal Storage This would allow a `Progress` object to be stored in Swift `TaskLocal` storage. This allows the implicit model of building a progress tree to be used from Swift Concurrency asynchronous contexts. In this solution, getting the current `Progress` and adding a child `Progress` is done by first reading from TaskLocal storage when called from a Swift Concurrency context. This method was found to be not preferable as we would like to encourage the usage of the explicit model of Progress Reporting, in which we do not depend on an implicit TaskLocal storage and have methods that report progress to explicitly accepts a `Progress` object as a parameter. -### Add Convenience Method to Foundation's `Progress` for Easier Instantiation of Child Progress +### 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: ```swift @@ -985,6 +841,27 @@ extension Progress { ``` This method would mean that we are altering the usage pattern of pre-existing `Progress` API, which may introduce more confusions to developers in their efforts to move from non-async functions to async functions. -## Acknowledgements +### Allow For Assignment of `ProgressReporter` to Multiple Progress Reporter Trees +The ability to assign a `ProgressReporter` to be part of multiple progress trees means allowing for a `ProgressReporter` to have more than one parent, would enable developers the flexibility to model any type of progress relationships. + +However, allowing the freedom to add a ProgressReporter to more than one tree may compromise the safety guarantee we want to provide in this API. The main safety guarantee we provide via this API is that `ProgressReporter` will not be used more than once because it is always instantiated from calling reporter(totalCount:) on a ~Copyable `ProgressReporter.Progress` instance. -Thanks to [Tony Parker](https://github.com/parkera) and [Tina Liu](https://github.com/itingliu) for constant feedback and guidance throughout to help shape this API and proposal. I would also like to thank [Jeremy Schonfeld](https://github.com/jmschonfeld), [Cassie Jones](https://github.com/porglezomp), [Konrad Malawski](https://github.com/ktoso), [Philippe Hausler](https://github.com/phausler), Julia Vashchenko for valuable feedback on this proposal and its previous versions. +## Acknowledgements +Thanks to +- [Tony Parker](https://github.com/parkera), +- [Tina Liu](https://github.com/itingliu), +- [Jeremy Schonfeld](https://github.com/jmschonfeld), +- [Charles Hu](https://github.com/iCharlesHu) +for constant feedback and guidance throughout to help shape this API and proposal. + +Thanks to +- [Cassie Jones](https://github.com/porglezomp), +- [Konrad Malawski](https://github.com/ktoso), +- [Philippe Hausler](https://github.com/phausler), +- [Julia Vashchenko] +for valuable feedback on this proposal and its previous versions. + +Thanks to +- [Konrad Malawski](https://github.com/ktoso), +- [Matt Ricketson](https://github.com/ricketson) +for prior efforts on ideation of a progress reporting mechanism compatible with Swift concurrency. From 21dffe988a31e1fef04ddcaaaabfbd2824692401 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 4 Apr 2025 12:10:15 -0700 Subject: [PATCH 07/29] update proposal to latest version --- Proposals/NNNN-progress-reporter.md | 42 ++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/NNNN-progress-reporter.md index 588eda585..9f8984a79 100644 --- a/Proposals/NNNN-progress-reporter.md +++ b/Proposals/NNNN-progress-reporter.md @@ -261,9 +261,7 @@ extension ImageProcessor { // Initialize file-related properties on the reporter reporter.withProperties { properties in properties.totalFileCount = images.count - properties.completedFileCount = 0 properties.totalByteCount = images.map { $0.bytes }.reduce(0, +) - properties.completedByteCount = 0 } for image in images { @@ -271,13 +269,8 @@ extension ImageProcessor { reporter.complete(count: 1) // Update each file-related property reporter.withProperties { properties in - if let completedFileCount = properties.completedFileCount, let completedByteCount = properties.completedByteCount { - properties.completedFileCount = completedFileCount + 1 - properties.completedByteCount = completedByteCount + image.bytes - } else { - properties.completedFileCount = 1 - properties.completedByteCount = image.bytes - } + properties.completedFileCount += 1 + properties.completedByteCount += image.bytes } } } @@ -474,6 +467,9 @@ func progressParentReporterChildInterop() { public protocol Property { associatedtype T : Sendable + + /// The default value to return when property is not set to a specific value. + static var defaultValue: T { get } /// Aggregates an array of `T` into a single value `T`. /// - Parameter all: Array of `T` to be aggregated. @@ -482,7 +478,7 @@ func progressParentReporterChildInterop() { } /// A container that holds values for properties that convey information about progress. - @dynamicMemberLookup public struct Values : Sendable { + @dynamicMemberLookup public struct Values : Sendable, CustomStringDebugConvertible { /// The total units of work. public var totalCount: Int? { mutating get set } @@ -490,8 +486,11 @@ func progressParentReporterChildInterop() { /// The completed units of work. public var completedCount: Int { mutating get set } - /// Returns a property value that a key path indicates. - public subscript

(dynamicMember key: KeyPath) -> P.T? where P : ProgressReporter.ProgressReporter.Property { 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) -> P.T where P : ProgressReporter.Property { get set } + + /// Returns a debug description. + public static var debugDescription: String { get } } /// Initializes `self` with `totalCount`. @@ -512,6 +511,9 @@ func progressParentReporterChildInterop() { /// Accesses or mutates any properties that convey additional information about progress. public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T + + /// Returns a debug description. + public static var debugDescription: String { get } } /// Default implementation for `reduce` where T is `AdditiveArithmetic`. @@ -536,6 +538,8 @@ For developers that would like to report additional metadata or properties as th ```swift @available(FoundationPreview 6.2, *) +extension ProgressReporter { + extension ProgressReporter { public struct Properties { @@ -546,6 +550,8 @@ extension ProgressReporter { public struct TotalFileCount : Property { public typealias T = Int + + public static var defaultValue: Int { get } } /// The number of completed files. @@ -554,6 +560,8 @@ extension ProgressReporter { public struct CompletedFileCount : Property { public typealias T = Int + + public static var defaultValue: Int { get } } /// The total number of bytes. @@ -562,6 +570,8 @@ extension ProgressReporter { public struct TotalByteCount : Property { public typealias T = UInt64 + + public static var defaultValue: UInt64 { get } } /// The number of completed bytes. @@ -570,6 +580,8 @@ extension ProgressReporter { public struct CompletedByteCount : Property { public typealias T = UInt64 + + public static var defaultValue: UInt64 { get } } /// The throughput, in bytes per second. @@ -578,6 +590,8 @@ extension ProgressReporter { public struct Throughput : Property { public typealias T = UInt64 + + public static var defaultValue: UInt64 { get } } /// The amount of time remaining in the processing of files. @@ -586,6 +600,8 @@ extension ProgressReporter { public struct EstimatedTimeRemaining : Property { public typealias T = Duration + + public static var defaultValue: Duration { get } } } } @@ -858,7 +874,7 @@ Thanks to - [Cassie Jones](https://github.com/porglezomp), - [Konrad Malawski](https://github.com/ktoso), - [Philippe Hausler](https://github.com/phausler), -- [Julia Vashchenko] +- Julia Vashchenko for valuable feedback on this proposal and its previous versions. Thanks to From 93b855d28bc98b5a11ce7ccb93f84c46ffcf362b Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 4 Apr 2025 12:11:24 -0700 Subject: [PATCH 08/29] fix spacing --- Proposals/NNNN-progress-reporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/NNNN-progress-reporter.md index 9f8984a79..a6a66c9a8 100644 --- a/Proposals/NNNN-progress-reporter.md +++ b/Proposals/NNNN-progress-reporter.md @@ -874,7 +874,7 @@ Thanks to - [Cassie Jones](https://github.com/porglezomp), - [Konrad Malawski](https://github.com/ktoso), - [Philippe Hausler](https://github.com/phausler), -- Julia Vashchenko +- Julia Vashchenko for valuable feedback on this proposal and its previous versions. Thanks to From 720cf0c2729e9267d4b0a3b76674e225e39ca92a Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 18 Apr 2025 16:53:47 -0700 Subject: [PATCH 09/29] v3 updates --- Proposals/NNNN-progress-reporter.md | 247 +++++++++++++++++----------- 1 file changed, 152 insertions(+), 95 deletions(-) diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/NNNN-progress-reporter.md index a6a66c9a8..f410133e7 100644 --- a/Proposals/NNNN-progress-reporter.md +++ b/Proposals/NNNN-progress-reporter.md @@ -14,6 +14,10 @@ - Replaced top level `totalCount` to be get-only and only settable via `withProperties` closure - Added the ability for `completedCount` to be settable via `withProperties` closure - Omitted checking of `Task.cancellation` in `complete(count:)` method +* **v3** Major Updates: + - Renamed `ProgressReporter.Progress` struct to `Subprogress` + - Renamed `assign(count:)` method to `subprogress(assigningCount:)` + - Restructure examples in `Proposed Solution` to showcase clearer difference of progress-reporting framework code and progress-observing developer code ## Table of Contents @@ -22,12 +26,12 @@ * [Proposed Solution and Example](#proposed-solution-and-example) * [Reporting Progress (General Operations)](#reporting-progress-general-operations) * [Reporting Progress (File-Related Operations)](#reporting-progress-file\-related-operations) - * [Advantages of using `ProgressReporter.Progress` as Currency Type](#advantages-of-using-progresssreporterprogress-as-currency-type) + * [Advantages of using `Subprogress` as Currency Type](#advantages-of-using-subprogress-as-currency-type) * [Interoperability with Existing `Progress`](#interoperability-with-existing-progress) * [Detailed Design](#detailed-design) * [`ProgressReporter`](#progressreporter) * [`ProgressReporter.Properties`](#progressreporterproperties) - * [`ProgressReporter.Progress`](#progressreporterprogress) + * [`Subprogress`](#subprogress) * [`ProgressReporter.FormatStyle`](#progressreporterformatstyle) * [`ProgressReporter.FileFormatStyle`](#progressreporterfileformatstyle) * [Interoperability with Existing `Progress`](#methods-for-interoperability-with-existing-progress) @@ -52,6 +56,8 @@ * [Store Existing `Progress` in TaskLocal Storage](#store-existing-progress-in-tasklocal-storage) * [Add Convenience Method to Existing `Progress` for Easier Instantiation of Child Progress](#add-convenience-method-to-existing-progress-for-easier-instantiation-of-child-progress) * [Allow for Assignment of `ProgressReporter` to Multiple Progress Reporter Trees](#allow-for-assignment-of-progressreporter-to-multiple-progress-reporter-trees) + * [Replace Count\-based Relationships between `ProgressReporter`](#replace-countbased-relationships-between-progressreporter) + * [Introduce Additional Convenience for Getting `Subprogress`](#introduce-additional-convenience-for-getting-subprogress) * [Acknowledgements](#acknowledgements) ## Introduction @@ -139,13 +145,13 @@ The existing `Progress` was not designed in a way that enforces the usage of `Pr We introduce a new progress reporting mechanism following the new `ProgressReporter` type. This type encourages safer practices of progress reporting, separating what to be passed as parameter from what to be used to report progress. -This proposal outlines the use of `ProgressReporter` as reporters of progress and `~Copyable` `ProgressReporter.Progress` as parameters passed to progress reporting methods. +This proposal outlines the use of `ProgressReporter` as reporters of progress and `~Copyable` `Subprogress` as parameters passed to progress reporting methods. ## Proposed solution and example ### Reporting Progress (General Operations) -To begin, let's create a class called `MakeSalad` that reports progress made on a salad while it is being made. +To begin, let's create a framework called `SaladMaker` that contains functionalities that can make a salad and has built-in progress reporting. ```swift struct Fruit { @@ -156,56 +162,72 @@ struct Dressing { func pour() async { ... } } -public class MakeSalad { +public class SaladMaker { - let overall: ProgressReporter let fruits: [Fruit] let dressings: [Dressing] public init() { - overall = ProgressReporter(totalCount: 100) - ... + fruits = [Fruit("apple"), Fruit("banana"), Fruit("cherry")] + dressings = [Dressing("mayo"), Dressing("mustard"), Dressing("ketchup")] } } ``` -In order to report progress on subparts of making a salad, such as `chopFruits` and `mixDressings`, we pass an instance of `ProgressReporter.Progress` to each subpart. Each `ProgressReporter.Progress` passed into the subparts then has to be consumed to initialize an instance of `ProgressReporter`. This is done by calling `reporter(totalCount:)` on `ProgressReporter.Progress`. These `ProgressReporter`s of subparts will contribute to the `overall` progress reporter within the class, due to established parent-children relationships between `overall` and the reporters of subparts. This can be done as follows: +In order to report progress on subparts of making a salad, such as `chopFruits` and `mixDressings`, the framework methods each has a `Subprogress` parameter. The `Subprogress` parameter is also optional to provide developers the option to either opt-in to receiving progress updates when calling each of the methods. + +Within the methods thay report progress, each `Subprogress` passed into the subparts then has to be consumed to initialize an instance of `ProgressReporter`. This is done by calling `reporter(totalCount:)` on `Subprogress`. This can be done as follows: ```swift -extension MakeSalad { +extension SaladMaker { - public func start() async -> String { - // Assign a `ProgressReporter.Progress` instance with 70 portioned count from `overall` to `chopFruits` method - await chopFruits(progress: overall.assign(count: 70)) - - // Assign a `ProgressReporter.Progress` instance with 30 portioned count from `overall` to `mixDressings` method - await mixDressings(progress: overall.assign(count: 30)) - - return "Salad is ready!" - } - - private func chopFruits(progress: consuming ProgressReporter.Progress) async { + private func chopFruits(progress: consuming Subprogress?) async { // Initialize a progress reporter to report progress on chopping fruits // with passed-in progress parameter - let choppingReporter = progress.reporter(totalCount: fruits.count) + let choppingReporter = progress?.reporter(totalCount: fruits.count) for fruit in fruits { await fruit.chop() - choppingReporter.complete(count: 1) + choppingReporter?.complete(count: 1) } } - private func mixDressings(progress: consuming ProgressReporter.Progress) async { + private func mixDressings(progress: consuming Subprogress?) async { // Initialize a progress reporter to report progress on mixing dressing // with passed-in progress parameter - let dressingReporter = progress.reporter(totalCount: dressings.count) + let dressingReporter = progress?.reporter(totalCount: dressings.count) for dressing in dressings { await dressing.pour() - dressingReporter.complete(count: 1) + dressingReporter?.complete(count: 1) } } } ``` +When a developer wants to use the `SaladMaker` framework and track the progress of making a salad, they can do so as follows: + +```swift +func makeSalad() async { + let saladMaker = SaladMaker() + + // Initialize a root-level `ProgressReporter` representing overall progress + let overall = ProgressReporter(totalCount: 100) + + // Call `chopFruits` and opt-in to receive progress updates + // by passing in a `Subprogress` constituting 70 count of overall progress + await saladMaker.chopFruits(progress: overall.subprogress(assigningCount: 70)) + + print("Chopped fruits, salad is \(overall.formatted(.fractionCompleted()))") + + // Call `mixDressings` and opt-in to receive progress updates + // by passing in a `Subprogress` constituting 30 count of overall progress + await saladMaker.mixDressings(progress: overall.subprogress(assigningCount: 30)) + + print("Mixed dressings, salad is \(overall.formatted(.fractionCompleted()))") +} + +await makeSalad() +``` + ### Reporting Progress (File-Related Operations) With the use of @dynamicMemberLookup attribute, `ProgressReporter` is able to access properties that are not explicitly defined in the class. This means that developers are able to define additional properties on the class specific to the operations they are reporting progress on. For instance, we pre-define additional file-related properties on `ProgressReporter` by extending `ProgressReporter` for use cases of reporting progress on file operations. @@ -214,7 +236,7 @@ With the use of @dynamicMemberLookup attribute, `ProgressReporter` is able to ac In this section, we will show an example of how we report progress with additional file-related properties: -To begin, let's create a class `ImageProcessor` that first downloads images, then applies a filter on all the images downloaded. We can track the progress of this operation in two subparts, so we begin by instantiating an overall `ProgressReporter` with a total count of 2. +To begin, let's create a class `ImageProcessor` that has the functionalities of downloading images and applying a filter onto images. ```swift struct Image { @@ -227,132 +249,144 @@ struct Image { final class ImageProcessor: Sendable { - let overall: ProgressReporter let images: [Image] - init() { - overall = ProgressReporter(totalCount: 2) - images = [Image(bytes: 1000), Image(bytes: 2000), Image(bytes: 3000)] + init(images: [Image]) { + self.images = images } } ``` -In order to report progress on the subparts of downloading images and applying filter onto the images, we assign 1 count of `overall`'s `totalCount` to each subpart. - -The subpart of downloading images contains information such as `totalByteCount` that we want to report along with the properties directly defined on a `ProgressReporter`. While `totalByteCount` is not directly defined on the `ProgressReporter` class, we can still set the property `totalByteCount` via the `withProperties` closure because this property can be discovered at runtime via the `@dynamicMemberLookup` attribute. +The method to download images would also report information such as `totalByteCount` along with the properties directly defined on a `ProgressReporter`. While `totalByteCount` is not directly defined on the `ProgressReporter` class, we can still set the property `totalByteCount` via the `withProperties` closure because this property can be discovered at runtime via the `@dynamicMemberLookup` attribute. The subpart of applying filter does not contain additional file-related information, so we report progress on this subpart as usual. +Both the `downloadImagesFromDisk` and `applyFilterToImages` methods allow developers the option to receive progress updates while the tasks are carried out. + ```swift extension ImageProcessor { - func downloadImagesFromDiskAndApplyFilter() async { - // Assign a `ProgressReporter.Progress` instance with 1 portioned count from `overall` to `downloadImagesFromDisk` - await downloadImagesFromDisk(progress: overall.assign(count: 1)) - - // Assign a `ProgressReporter.Progress` instance with 1 portioned count from `overall` to `applyFilterToImages` - await applyFilterToImages(progress: overall.assign(count: 1)) - } - func downloadImagesFromDisk(progress: consuming ProgressReporter.Progress) async { + func downloadImagesFromDisk(progress: consuming Subprogress?) async { // Initialize a progress reporter to report progress on downloading images // with passed-in progress parameter - let reporter = progress.reporter(totalCount: images.count) + let reporter = progress?.reporter(totalCount: images.count) // Initialize file-related properties on the reporter - reporter.withProperties { properties in + reporter?.withProperties { properties in properties.totalFileCount = images.count properties.totalByteCount = images.map { $0.bytes }.reduce(0, +) } for image in images { await image.download() - reporter.complete(count: 1) + reporter?.complete(count: 1) // Update each file-related property - reporter.withProperties { properties in + reporter?.withProperties { properties in properties.completedFileCount += 1 properties.completedByteCount += image.bytes } } } - func applyFilterToImages(progress: consuming ProgressReporter.Progress) async { + func applyFilterToImages(progress: consuming Subprogress?) async { // Initializes a progress reporter to report progress on applying filter // with passed-in progress parameter - let reporter = progress.reporter(totalCount: images.count) + let reporter = progress?.reporter(totalCount: images.count) for image in images { await image.applyFilter() - reporter.complete(count: 1) + reporter?.complete(count: 1) } } } ``` -### Advantages of using `ProgresssReporter.Progress` as Currency Type +When a developer wants to use the `ImageProcessor` framework to download images and apply filters on them, they can do so as follows: -The advantages of `ProgressReporter` mainly derive from the use of `ProgressReporter.Progress` as a currency to create descendants of `ProgresssReporter`, and the recommended ways to use `ProgressReporter.Progress` are as follows: +```swift +func downloadImagesAndApplyFilter() async { + let imageProcessor = ImageProcessor(images: [Image(bytes: 1000), Image(bytes: 2000), Image(bytes: 3000)]) + + // Initialize a root-level `ProgressReporter` representing overall progress + let overall = ProgressReporter(totalCount: 2) + + // Call `downloadImagesFromDisk` and opt-in to receive progress updates + // by passing in a `Subprogress` constituting 1 count of overall progress + await imageProcessor.downloadImagesFromDisk(progress: overall.subprogress(assigningCount: 1)) + + // Call `applyFilterToImages` and opt-in to receive progress updates + // by passing in a `Subprogress` constituting 1 count of overall progress + await imageProcessor.applyFilterToImages(progress: overall.subprogress(assigningCount: 1)) +} + +await downloadImagesAndApplyFilter() +``` + +### Advantages of using `Subprogress` as Currency Type + +The advantages of `ProgressReporter` mainly derive from the use of `Subprogress` as a currency to create descendants of `ProgresssReporter`, and the recommended ways to use `Subprogress` are as follows: -1. Pass `ProgressReporter.Progress` instead of `ProgressReporter` as a parameter to methods that report progress. +1. Pass `Subprogress` instead of `ProgressReporter` as a parameter to methods that report progress. -`ProgressReporter.Progress` should be used as the currency to be passed into progress-reporting methods, within which a child `ProgressReporter` instance that constitutes a portion of its parent's total units is created via a call to `reporter(totalCount:)`, as follows: +`Subprogress` should be used as the currency to be passed into progress-reporting methods, within which a child `ProgressReporter` instance that constitutes a portion of its parent's total units is created via a call to `reporter(totalCount:)`, as follows: ```swift func correctlyReportToSubprogressAfterInstantiatingReporter() async { let overall = ProgressReporter(totalCount: 2) - await subTask(progress: overall.assign(count: 1)) + await subTask(progress: overall.subprogress(assigningCount: 1)) } -func subTask(progress: consuming ProgressReporter.Progress) async { +func subTask(progress: consuming Subprogress) async { let count = 10 - let progressReporter = progress.reporter(totalCount: count) // returns an instance of ProgressReporter that can be used to report subprogress + let progressReporter = progress.reporter(totalCount: count) // returns an instance of ProgressReporter for _ in 1...count { progressReporter?.complete(count: 1) // reports progress as usual } } ``` -While developers may accidentally make the mistake of trying to report progress to a passed-in `ProgressReporter.Progress`, the fact that it does not have the same properties as an actual `ProgressReporter` means the compiler can inform developers when they are using either `ProgressReporter` or `ProgressReporter.Progress` wrongly. The only way for developers to kickstart actual progress reporting with `ProgressReporter.Progress` is by calling the `reporter(totalCount:)` to create a `ProgressReporter`, then subsequently call `complete(count:)` on `ProgressReporter`. +While developers may accidentally make the mistake of trying to report progress to a passed-in `Subprogress`, the fact that it does not have the same properties as an actual `ProgressReporter` means the compiler can inform developers when they are using either `ProgressReporter` or `Subprogress` wrongly. The only way for developers to kickstart actual progress reporting with `Subprogress` is by calling the `reporter(totalCount:)` to create a `ProgressReporter`, then subsequently call `complete(count:)` on `ProgressReporter`. Each time before progress reporting happens, there needs to be a call to `reporter(totalCount:)`, which returns a `ProgressReporter` instance, before calling `complete(count:)` on the returned `ProgressReporter`. -The following faulty example shows how reporting progress directly to `ProgressReporter.Progress` without initializing it will be cause a compiler error. Developers will always need to instantiate a `ProgressReporter` from `ProgresReporter.Progress` before reporting progress. +The following faulty example shows how reporting progress directly to `Subprogress` without initializing it will be cause a compiler error. Developers will always need to instantiate a `ProgressReporter` from `ProgresReporter.Progress` before reporting progress. ```swift func incorrectlyReportToSubprogressWithoutInstantiatingReporter() async { let overall = ProgressReporter(totalCount: 2) - await subTask(progress: overall.assign(count: 1)) + await subTask(progress: overall.subprogress(assigningCount: 1)) } -func subTask(progress: consuming ProgressReporter.Progress) async { - // COMPILER ERROR: Value of type 'ProgressReporter.Progress' has no member 'complete' +func subTask(progress: consuming Subprogress) async { + // COMPILER ERROR: Value of type 'Subprogress' has no member 'complete' progress.complete(count: 1) } ``` -2. Consume each `ProgressReporter.Progress` only once, and if not consumed, its parent `ProgressReporter` behaves as if none of its units were ever allocated to create `ProgressReporter.Progress`. +2. Consume each `Subprogress` only once, and if not consumed, its parent `ProgressReporter` behaves as if none of its units were ever allocated to create `Subprogress`. -Developers should create only one `ProgressReporter.Progress` for a corresponding to-be-instantiated `ProgressReporter` instance, as follows: +Developers should create only one `Subprogress` for a corresponding to-be-instantiated `ProgressReporter` instance, as follows: ```swift func correctlyConsumingSubprogress() { let overall = ProgressReporter(totalCount: 2) - let progressOne = overall.assign(count: 1) // create one ProgressReporter.Progress + let progressOne = overall.subprogress(assigningCount: 1) // create one Subprogress let reporterOne = progressOne.reporter(totalCount: 10) // initialize ProgressReporter instance with 10 units - let progressTwo = overall.assign(count: 1) //create one ProgressReporter.Progress + let progressTwo = overall.subprogress(assigningCount: 1) //create one Subprogress let reporterTwo = progressTwo.reporter(totalCount: 8) // initialize ProgressReporter instance with 8 units } ``` -It is impossible for developers to accidentally consume `ProgressReporter.Progress` more than once, because even if developers accidentally **type** out an expression to consume an already-consumed `ProgressReporter.Progress`, their code won't compile at all. +It is impossible for developers to accidentally consume `Subprogress` more than once, because even if developers accidentally **type** out an expression to consume an already-consumed `Subprogress`, their code won't compile at all. -The `reporter(totalCount:)` method, which **consumes** the `ProgressReporter.Progress`, can only be called once on each `ProgressReporter.Progress` instance. If there are more than one attempts to call `reporter(totalCount:)` on the same instance of `ProgressReporter.Progress`, the code will not compile due to the `~Copyable` nature of `ProgressReporter.Progress`. +The `reporter(totalCount:)` method, which **consumes** the `Subprogress`, can only be called once on each `Subprogress` instance. If there are more than one attempts to call `reporter(totalCount:)` on the same instance of `Subprogress`, the code will not compile due to the `~Copyable` nature of `Subprogress`. ```swift func incorrectlyConsumingSubprogress() { let overall = ProgressReporter(totalCount: 2) - let progressOne = overall.assign(count: 1) // create one ProgressReporter.Progress + let progressOne = overall.subprogress(assigningCount: 1) // create one Subprogress let reporterOne = progressOne.reporter(totalCount: 10) // initialize ProgressReporter instance with 10 units // COMPILER ERROR: 'progressOne' consumed more than once @@ -380,7 +414,7 @@ func doSomethingWithProgress() -> Progress { } // Framework code: Function reporting progress with `ProgressReporter` -func doSomethingWithReporter(progress: consuming ProgressReporter.Progress) async -> Int { +func doSomethingWithReporter(progress: consuming Subprogress) async -> Int { let reporter = progress.reporter(totalCount: 2) //do something reporter.complete(count: 1) @@ -389,25 +423,25 @@ func doSomethingWithReporter(progress: consuming ProgressReporter.Progress) asyn } ``` -In the case in which we need to receive a `Progress` instance and add it as a child to a `ProgressReporter` parent, we can use the interop method `assign(count: to:)`. +In the case in which we need to receive a `Progress` instance and add it as a child to a `ProgressReporter` parent, we can use the interop method `subprogress(assigningCount: to:)`. -The choice of naming the interop method as `assign(count: to:)` is to keep the syntax consistent with the method used to add a `ProgressReporter` instance to the progress tree, `assign(count:)`. An example of how these can be used to compose a `ProgressReporter` tree with a top-level `ProgressReporter` is as follows: +The choice of naming the interop method as `subprogress(assigningCount: to:)` is to keep the syntax consistent with the method used to add a `ProgressReporter` instance to the progress tree, `subprogress(assigningCount:)`. An example of how these can be used to compose a `ProgressReporter` tree with a top-level `ProgressReporter` is as follows: ```swift // Developer code func reporterParentProgressChildInterop() async { let overall = ProgressReporter(totalCount: 2) // Top-level `ProgressReporter` - // Assigning 1 unit of overall's `totalCount` to `ProgressReporter.Progress` - let progressOne = overall.assign(count: 1) - // Passing `ProgressReporter.Progress` to method reporting progress + // Assigning 1 unit of overall's `totalCount` to `Subprogress` + let progressOne = overall.subprogress(assigningCount: 1) + // Passing `Subprogress` to method reporting progress let result = await doSomethingWithReporter(progress: progressOne) // Getting a `Progress` from method reporting progress let progressTwo = doSomethingWithProgress() // Assigning 1 unit of overall's `totalCount` to the existing `Progress` - overall.assign(count: 1, to: progressTwo) + overall.subprogress(assigningCount: 1, to: progressTwo) } ``` @@ -425,9 +459,9 @@ func progressParentReporterChildInterop() { // Add Foundation's `Progress` as a child which takes up 1 unit of overall's `totalUnitCount` overall.addChild(progressOne, withPendingUnitCount: 1) - // Getting a `ProgressReporter.Progress` which takes up 1 unit of overall's `totalUnitCount` + // Getting a `Subprogress` which takes up 1 unit of overall's `totalUnitCount` let progressTwo = overall.makeChild(withPendingUnitCount: 1) - // Passing `ProgressReporter.Progress` instance to method reporting progress + // Passing `Subprogress` instance to method reporting progress doSomethingWithReporter(progress: progressTwo) } ``` @@ -499,11 +533,11 @@ func progressParentReporterChildInterop() { /// - Parameter totalCount: Total units of work. public convenience init(totalCount: Int?) - /// Returns a `ProgressReporter.Progress` representing a portion of `self`which can be passed to any method that reports progress. + /// Returns a `Subprogress` representing a portion of `self`which can be passed to any method that reports progress. /// - /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `ProgressReporter.Progress`. - /// - Returns: A `ProgressReporter.Progress` instance. - public func assign(count portionOfParent: Int) -> Progress + /// - Parameter count: Units, which is a portion of `totalCount` delegated to an instance of `Subprogress`. + /// - Returns: A `Subprogress` instance. + public func subprogress(assigningCount portionOfParent: Int) -> Subprogress /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. @@ -607,9 +641,9 @@ extension ProgressReporter { } ``` -### `ProgressReporter.Progress` +### `Subprogress` -An instance of `ProgressReporter.Progress` is returned from a call to `ProgressReporter`'s `assign(count:)`. `ProgressReporter.Progress` acts as an intermediary instance that you pass into functions that report progress. Additionally, callers should convert `ProgressReporter.Progress` to `ProgressReporter` before starting to report progress with it by calling `reporter(totalCount:)`. +An instance of `Subprogress` is returned from a call to `ProgressReporter`'s `subprogress(assigningCount:)`. `Subprogress` acts as an intermediary instance that you pass into functions that report progress. Additionally, callers should convert `Subprogress` to `ProgressReporter` before starting to report progress with it by calling `reporter(totalCount:)`. ```swift @available(FoundationPreview 6.2, *) @@ -730,7 +764,7 @@ To allow frameworks which may have dependencies on the pre-existing progress-rep #### ProgressReporter (Parent) - Progress (Child) -To add an instance of `Progress` as a child to an instance of `ProgressReporter`, we pass an `Int` for the portion of `ProgressReporter`'s `totalCount` `Progress` should take up and a `Progress` instance to `assign(count: to:)`. The `ProgressReporter` instance will track the `Progress` instance just like any of its `ProgressReporter` children. +To add an instance of `Progress` as a child to an instance of `ProgressReporter`, we pass an `Int` for the portion of `ProgressReporter`'s `totalCount` `Progress` should take up and a `Progress` instance to `subprogress(assigningCount: to:)`. The `ProgressReporter` instance will track the `Progress` instance just like any of its `ProgressReporter` children. ```swift @available(FoundationPreview 6.2, *) @@ -739,26 +773,26 @@ extension ProgressReporter { /// - Parameters: /// - count: Number of units delegated from `self`'s `totalCount`. /// - progress: `Progress` which receives the delegated `count`. - public func assign(count: Int, to progress: Foundation.Progress) + public func subprogress(assigningCount: Int, to progress: Foundation.Progress) } ``` #### Progress (Parent) - ProgressReporter (Child) -To add an instance of `ProgressReporter` as a child to an instance of the existing `Progress`, the `Progress` instance calls `makeChild(count:kind:)` to get a `ProgressReporter.Progress` instance that can be passed as a parameter to a function that reports progress. The `Progress` instance will track the `ProgressReporter` instance as a child, just like any of its `Progress` children. +To add an instance of `ProgressReporter` as a child to an instance of the existing `Progress`, the `Progress` instance calls `makeChild(count:kind:)` to get a `Subprogress` instance that can be passed as a parameter to a function that reports progress. The `Progress` instance will track the `ProgressReporter` instance as a child, just like any of its `Progress` children. ```swift @available(FoundationPreview 6.2, *) extension Progress { - /// Returns a ProgressReporter.Progress which can be passed to any method that reports progress + /// Returns a Subprogress which can be passed to any method that reports progress /// and can be initialized into a child `ProgressReporter` to the `self`. /// /// Delegates a portion of totalUnitCount to a future child `ProgressReporter` instance. /// /// - Parameter count: Number of units delegated to a child instance of `ProgressReporter` - /// which may be instantiated by `ProgressReporter.Progress` later when `reporter(totalCount:)` is called. - /// - Returns: A `ProgressReporter.Progress` instance. - public func makeChild(withPendingUnitCount count: Int) -> ProgressReporter.Progress + /// which may be instantiated by `Subprogress` later when `reporter(totalCount:)` is called. + /// - Returns: A `Subprogress` instance. + public func makeChild(withPendingUnitCount count: Int) -> Subprogress } ``` @@ -789,13 +823,19 @@ As the existing `Progress` already exists, we had to come up with a name other t We decided to proceed with the name `ProgressReporter` because prefixing an API with the term `Async` may be confusing for developers, as there is a precedent of APIs doing so, such as `AsyncSequence` adding asynchronicity to `Sequence`, whereas this is a different case for `ProgressReporter` vs `Progress`. -2. Alternative to `ProgressReporter.Progress` +2. Alternative to `Subprogress` - `ProgressReporter.Link` - `ProgressReporter.Child` - - `ProgressReporter.Token` + - `ProgressReporter.Token` + - `ProgressReporter.Progress` -While the names `Link`, `Child`, and `Token` may appeal to the fact that this is a type that is separate from the `ProgressReporter` itself and should only be used as a function parameter and to be consumed immediately to kickstart progress reporting, it is ambiguous because developers may not immedidately figure out its function from just the name itself. `Progress` is an intuitive name because developers will instinctively think of the term `Progress` when they want to adopt `ProgressReporting`. +While the names `Link`, `Child`, and `Token` may appeal to the fact that this is a type that is separate from the `ProgressReporter` itself and should only be used as a function parameter and to be consumed immediately to kickstart progress reporting, it is ambiguous because developers may not immedidately figure out its function from just the name itself. While `Progress` may be a good name to indicate to developers that any method receiving `Progress` as a parameter reports progress, it is does not accurately convey its nature of being the bearer of a certain portion of some parent's `totalCount`. We landed at `Subprogress` as it serves as an indicator for developers that methods with a `Subprogress` parameter reports progress, and at the same time conveys the correct idea that it is meant to be a part of a progress tree. +3. Alternative to `subprogress(assigningCount:)` + - `assign(count:)` + +We initially considered naming the method that returns a `Subprogress` instance `assign(count:)` due to its nature of being a peer method to `complete(count:)`. However, `assign` does not intuitively indicate to developers that this method is supposed to return anything, so we decided on naming the method `subprogress` and its argument `assigningCount` to indicate that it is assigning a portion of its own `totalCount` to a `Subprogress` instance. + ### Introduce `ProgressReporter` to Swift standard library In consideration for making `ProgressReporter` a lightweight API for server-side developers to use without importing the entire `Foundation` framework, we considered either introducing `ProgressReporter` in a standalone module, or including `ProgressReporter` in existing Swift standard library modules such as `Observation` or `Concurrency`. However, given the fact that `ProgressReporter` has dependencies in `Observation` and `Concurrency` modules, and that the goal is to eventually support progress reporting over XPC connections, `Foundation` framework is the most ideal place to host the `ProgressReporter` as it is the central framework for APIs that provide core functionalities when these functionalities are not provided by Swift standard library and its modules. @@ -857,10 +897,27 @@ extension Progress { ``` This method would mean that we are altering the usage pattern of pre-existing `Progress` API, which may introduce more confusions to developers in their efforts to move from non-async functions to async functions. -### Allow For Assignment of `ProgressReporter` to Multiple Progress Reporter Trees +### Allow for Assignment of `ProgressReporter` to Multiple Progress Reporter Trees The ability to assign a `ProgressReporter` to be part of multiple progress trees means allowing for a `ProgressReporter` to have more than one parent, would enable developers the flexibility to model any type of progress relationships. -However, allowing the freedom to add a ProgressReporter to more than one tree may compromise the safety guarantee we want to provide in this API. The main safety guarantee we provide via this API is that `ProgressReporter` will not be used more than once because it is always instantiated from calling reporter(totalCount:) on a ~Copyable `ProgressReporter.Progress` instance. +However, allowing the freedom to add a ProgressReporter to more than one tree compromises the safety guarantee we want to provide in this API. The main safety guarantee we provide via this API is that `ProgressReporter` will not be used more than once because it is always instantiated from calling reporter(totalCount:) on a ~Copyable `Subprogress` instance. + +### Replace Count-based Relationships between `ProgressReporter` +The progress-reporting functionality = of each `ProgressReporter` depends on the `totalCount` and `completedCount` properties, both of which are integers. This puts the responsibility onto the developers to make sure that all `assignedCount` add up to the `totalCount` for a correct progress reporting at the top level. + +While there are considerations to move away from this due to the extra attention required from developers in refactoring code, `fractionCompleted`, which is a `Double` value, has the most precision when computed from integers. + +### Introduce Additional Convenience for Getting `Subprogress` +We considered introducing a convenience for getting `Subprogress` by calling `subprogress()` without specifying `assigningCount` as an argument. In this case, the `Subprogress` returned will automatically be assigned 1 count of its parent's `totalCount` and parent's `totalCount` will automatically increase by 1. + +However, this convenience would introduce more confusion with developers when they try to use `subprogress()` and `subprogress(assigningCount:)` next to each other because `subprogress(assigningCount:)` does not automatically increase the parent's `totalCount`: + +```swift +// Developer code +let overall = ProgressReporter(totalCount: nil) +await doSomething(overall.assign()) // totalCount: nil -> 1, assignedCount: 0 -> 1 +await doSomething(overall.assign(count: 2)) // totalCount: 1 (doesn't increase), assignedCount: 1 -> 3 +``` ## Acknowledgements Thanks to From 8d6c552f9a76d74fae73fe131058833d1cd1a4d2 Mon Sep 17 00:00:00 2001 From: Charles Hu Date: Wed, 23 Apr 2025 11:07:04 -0700 Subject: [PATCH 10/29] Update review status for SF-0023 to active review --- ...-reporter.md => 0023-progress-reporter.md} | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) rename Proposals/{NNNN-progress-reporter.md => 0023-progress-reporter.md} (94%) diff --git a/Proposals/NNNN-progress-reporter.md b/Proposals/0023-progress-reporter.md similarity index 94% rename from Proposals/NNNN-progress-reporter.md rename to Proposals/0023-progress-reporter.md index f410133e7..e86e1ccc5 100644 --- a/Proposals/NNNN-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -1,9 +1,11 @@ # `ProgressReporter`: Progress Reporting in Swift Concurrency -* Proposal: SF-NNNN +* Proposal: SF-0023 * Author(s): [Chloe Yeo](https://github.com/chloe-yeo) -* Review Manager: TBD -* Status: **Pitch** +* Review Manager: [Charles Hu](https://github.com/iCharlesHu) +* Status: **Review Apr. 23, 2025...Apr. 30, 2025** +* Review: [Pitch](https://forums.swift.org/t/pitch-progress-reporting-in-swift-concurrency/78112/10) + ## Revision history @@ -21,44 +23,47 @@ ## Table of Contents -* [Introduction](#introduction) -* [Motivation](#motivation) -* [Proposed Solution and Example](#proposed-solution-and-example) - * [Reporting Progress (General Operations)](#reporting-progress-general-operations) - * [Reporting Progress (File-Related Operations)](#reporting-progress-file\-related-operations) - * [Advantages of using `Subprogress` as Currency Type](#advantages-of-using-subprogress-as-currency-type) - * [Interoperability with Existing `Progress`](#interoperability-with-existing-progress) -* [Detailed Design](#detailed-design) - * [`ProgressReporter`](#progressreporter) - * [`ProgressReporter.Properties`](#progressreporterproperties) - * [`Subprogress`](#subprogress) - * [`ProgressReporter.FormatStyle`](#progressreporterformatstyle) - * [`ProgressReporter.FileFormatStyle`](#progressreporterfileformatstyle) - * [Interoperability with Existing `Progress`](#methods-for-interoperability-with-existing-progress) - * [`ProgressReporter` \(Parent\) \- `Progress` \(Child\)](#progressreporter-parent---progress-child) - * [`Progress` \(Parent\) \- `ProgressReporter` \(Child\)](#progress-parent---progressreporter-child) -* [Impact on Existing Code](#impact-on-existing-code) -* [Future Directions](#future-directions) - * [ProgressView Overloads](#progressview-overloads) - * [Distributed ProgressReporter](#distributed-progressreporter) - * [Enhanced `FormatStyle`](#enhanced-formatstyle) -* [Alternatives Considered](#alternatives-considered) - * [Alternative Names](#alternative-names) - * [Introduce `ProgressReporter` to Swift standard library](#introduce-progressreporter-to-swift-standard-library) - * [Implement `ProgressReporter` as a Generic Class](#implement-progressreporter-as-a-generic-class) - * [Implement `ProgressReporter` as an Actor](#implement-progressreporter-as-an-actor) - * [Implement `ProgressReporter` as a Protocol](#implement-progressreporter-as-a-protocol) - * [Introduce an `Observable` Adapter for `ProgressReporter`](#introduce-an-observable-adapter-for-progressreporter) - * [Introduce Method to Generate Localized Description](#introduce-method-to-generate-localized-description) - * [Introduce Explicit Support for Cancellation, Pausing, Resuming of `ProgressReporter`](#introduce-explicit-support-for-cancellation-pausing-and-resuming-of-progressreporter) - * [Check Task Cancellation within `complete(count:)` Method](#check-task-cancellation-within-completecount-method) - * [Introduce totalCount and completedCount Properties as UInt64](#introduce-totalcount-and-completedcount-properties-as-uint64) - * [Store Existing `Progress` in TaskLocal Storage](#store-existing-progress-in-tasklocal-storage) - * [Add Convenience Method to Existing `Progress` for Easier Instantiation of Child Progress](#add-convenience-method-to-existing-progress-for-easier-instantiation-of-child-progress) - * [Allow for Assignment of `ProgressReporter` to Multiple Progress Reporter Trees](#allow-for-assignment-of-progressreporter-to-multiple-progress-reporter-trees) - * [Replace Count\-based Relationships between `ProgressReporter`](#replace-countbased-relationships-between-progressreporter) - * [Introduce Additional Convenience for Getting `Subprogress`](#introduce-additional-convenience-for-getting-subprogress) -* [Acknowledgements](#acknowledgements) +- [`ProgressReporter`: Progress Reporting in Swift Concurrency](#progressreporter-progress-reporting-in-swift-concurrency) + - [Revision history](#revision-history) + - [Table of Contents](#table-of-contents) + - [Introduction](#introduction) + - [Motivation](#motivation) + - [Proposed solution and example](#proposed-solution-and-example) + - [Reporting Progress (General Operations)](#reporting-progress-general-operations) + - [Reporting Progress (File-Related Operations)](#reporting-progress-file-related-operations) + - [Advantages of using `Subprogress` as Currency Type](#advantages-of-using-subprogress-as-currency-type) + - [Interoperability with Existing `Progress`](#interoperability-with-existing-progress) + - [Detailed design](#detailed-design) + - [`ProgressReporter`](#progressreporter) + - [`ProgressReporter.Properties`](#progressreporterproperties) + - [`Subprogress`](#subprogress) + - [`ProgressReporter.FormatStyle`](#progressreporterformatstyle) + - [`ProgressReporter.FileFormatStyle`](#progressreporterfileformatstyle) + - [Methods for Interoperability with Existing `Progress`](#methods-for-interoperability-with-existing-progress) + - [ProgressReporter (Parent) - Progress (Child)](#progressreporter-parent---progress-child) + - [Progress (Parent) - ProgressReporter (Child)](#progress-parent---progressreporter-child) + - [Impact on existing code](#impact-on-existing-code) + - [Future Directions](#future-directions) + - [Additional Overloads to APIs within UI Frameworks](#additional-overloads-to-apis-within-ui-frameworks) + - [Distributed `ProgressReporter`](#distributed-progressreporter) + - [Enhanced `FormatStyle`](#enhanced-formatstyle) + - [Alternatives considered](#alternatives-considered) + - [Alternative Names](#alternative-names) + - [Introduce `ProgressReporter` to Swift standard library](#introduce-progressreporter-to-swift-standard-library) + - [Implement `ProgressReporter` as a Generic Class](#implement-progressreporter-as-a-generic-class) + - [Implement `ProgressReporter` as an actor](#implement-progressreporter-as-an-actor) + - [Implement `ProgressReporter` as a protocol](#implement-progressreporter-as-a-protocol) + - [Introduce an `Observable` adapter for `ProgressReporter`](#introduce-an-observable-adapter-for-progressreporter) + - [Introduce Method to Generate Localized Description](#introduce-method-to-generate-localized-description) + - [Introduce Explicit Support for Cancellation, Pausing, and Resuming of `ProgressReporter`](#introduce-explicit-support-for-cancellation-pausing-and-resuming-of-progressreporter) + - [Check Task Cancellation within `complete(count:)` Method](#check-task-cancellation-within-completecount-method) + - [Introduce `totalCount` and `completedCount` properties as `UInt64`](#introduce-totalcount-and-completedcount-properties-as-uint64) + - [Store Existing `Progress` in TaskLocal Storage](#store-existing-progress-in-tasklocal-storage) + - [Add Convenience Method to Existing `Progress` for Easier Instantiation of Child Progress](#add-convenience-method-to-existing-progress-for-easier-instantiation-of-child-progress) + - [Allow for Assignment of `ProgressReporter` to Multiple Progress Reporter Trees](#allow-for-assignment-of-progressreporter-to-multiple-progress-reporter-trees) + - [Replace Count-based Relationships between `ProgressReporter`](#replace-count-based-relationships-between-progressreporter) + - [Introduce Additional Convenience for Getting `Subprogress`](#introduce-additional-convenience-for-getting-subprogress) + - [Acknowledgements](#acknowledgements) ## Introduction From 7d92d2d5dbfeb52527f72d3f0ca4dd93779c9c2b Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 20 May 2025 20:04:07 -0700 Subject: [PATCH 11/29] Round 2 Pitch --- Proposals/0023-progress-reporter.md | 973 +++++++++++++--------------- 1 file changed, 465 insertions(+), 508 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index e86e1ccc5..0dd3cbc23 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -3,7 +3,7 @@ * Proposal: SF-0023 * Author(s): [Chloe Yeo](https://github.com/chloe-yeo) * Review Manager: [Charles Hu](https://github.com/iCharlesHu) -* Status: **Review Apr. 23, 2025...Apr. 30, 2025** +* Status: **Pitch** * Review: [Pitch](https://forums.swift.org/t/pitch-progress-reporting-in-swift-concurrency/78112/10) @@ -16,73 +16,69 @@ - Replaced top level `totalCount` to be get-only and only settable via `withProperties` closure - Added the ability for `completedCount` to be settable via `withProperties` closure - Omitted checking of `Task.cancellation` in `complete(count:)` method -* **v3** Major Updates: +* **v3** Minor Updates: - Renamed `ProgressReporter.Progress` struct to `Subprogress` - Renamed `assign(count:)` method to `subprogress(assigningCount:)` - Restructure examples in `Proposed Solution` to showcase clearer difference of progress-reporting framework code and progress-observing developer code - +* **v4** Major Updates: + - Renamed `ProgressReporter` class to `ProgressManager` + - 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 + - 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 + ## Table of Contents -- [`ProgressReporter`: Progress Reporting in Swift Concurrency](#progressreporter-progress-reporting-in-swift-concurrency) - - [Revision history](#revision-history) - - [Table of Contents](#table-of-contents) - - [Introduction](#introduction) - - [Motivation](#motivation) - - [Proposed solution and example](#proposed-solution-and-example) - - [Reporting Progress (General Operations)](#reporting-progress-general-operations) - - [Reporting Progress (File-Related Operations)](#reporting-progress-file-related-operations) - - [Advantages of using `Subprogress` as Currency Type](#advantages-of-using-subprogress-as-currency-type) - - [Interoperability with Existing `Progress`](#interoperability-with-existing-progress) - - [Detailed design](#detailed-design) - - [`ProgressReporter`](#progressreporter) - - [`ProgressReporter.Properties`](#progressreporterproperties) - - [`Subprogress`](#subprogress) - - [`ProgressReporter.FormatStyle`](#progressreporterformatstyle) - - [`ProgressReporter.FileFormatStyle`](#progressreporterfileformatstyle) - - [Methods for Interoperability with Existing `Progress`](#methods-for-interoperability-with-existing-progress) - - [ProgressReporter (Parent) - Progress (Child)](#progressreporter-parent---progress-child) - - [Progress (Parent) - ProgressReporter (Child)](#progress-parent---progressreporter-child) - - [Impact on existing code](#impact-on-existing-code) - - [Future Directions](#future-directions) - - [Additional Overloads to APIs within UI Frameworks](#additional-overloads-to-apis-within-ui-frameworks) - - [Distributed `ProgressReporter`](#distributed-progressreporter) - - [Enhanced `FormatStyle`](#enhanced-formatstyle) - - [Alternatives considered](#alternatives-considered) - - [Alternative Names](#alternative-names) - - [Introduce `ProgressReporter` to Swift standard library](#introduce-progressreporter-to-swift-standard-library) - - [Implement `ProgressReporter` as a Generic Class](#implement-progressreporter-as-a-generic-class) - - [Implement `ProgressReporter` as an actor](#implement-progressreporter-as-an-actor) - - [Implement `ProgressReporter` as a protocol](#implement-progressreporter-as-a-protocol) - - [Introduce an `Observable` adapter for `ProgressReporter`](#introduce-an-observable-adapter-for-progressreporter) - - [Introduce Method to Generate Localized Description](#introduce-method-to-generate-localized-description) - - [Introduce Explicit Support for Cancellation, Pausing, and Resuming of `ProgressReporter`](#introduce-explicit-support-for-cancellation-pausing-and-resuming-of-progressreporter) - - [Check Task Cancellation within `complete(count:)` Method](#check-task-cancellation-within-completecount-method) - - [Introduce `totalCount` and `completedCount` properties as `UInt64`](#introduce-totalcount-and-completedcount-properties-as-uint64) - - [Store Existing `Progress` in TaskLocal Storage](#store-existing-progress-in-tasklocal-storage) - - [Add Convenience Method to Existing `Progress` for Easier Instantiation of Child Progress](#add-convenience-method-to-existing-progress-for-easier-instantiation-of-child-progress) - - [Allow for Assignment of `ProgressReporter` to Multiple Progress Reporter Trees](#allow-for-assignment-of-progressreporter-to-multiple-progress-reporter-trees) - - [Replace Count-based Relationships between `ProgressReporter`](#replace-count-based-relationships-between-progressreporter) - - [Introduce Additional Convenience for Getting `Subprogress`](#introduce-additional-convenience-for-getting-subprogress) - - [Acknowledgements](#acknowledgements) +* [Introduction](#introduction) +* [Motivation](#motivation) +* [Proposed Solution and Example](#proposed-solution-and-example) +* [Detailed Design](#detailed-design) +* [Impact on Existing Code](#impact-on-existing-code) +* [Future Directions](#future-directions) +* [Alternatives Considered](#alternatives-considered) +* [Acknowledgements](#acknowledgements) ## Introduction Progress reporting is a generally useful concept, and can be helpful in all kinds of applications: from high level UIs, to simple command line tools, and more. -Foundation offers a progress reporting mechanism that has been very popular with application developers on Apple platforms. The existing `Progress` class provides a self-contained, tree-based mechanism for progress reporting and is adopted in various APIs which are able to report progress. The functionality of the `Progress` class is two-fold –– it reports progress at the code level, and at the same time, displays progress at the User Interface level. While the recommended usage pattern of `Progress` works well with Cocoa's completion-handler-based async APIs, it does not fit well with Swift's concurrency support via async/await. +Foundation offers a progress reporting mechanism that has been popular with application developers on Apple platforms. The existing `Progress` class provides a self-contained, tree-based mechanism for progress reporting and is adopted in various APIs to report progress. While the recommended usage pattern of `Progress` works well with Cocoa's completion-handler-based async APIs, it does not fit well with Swift's concurrency support via async/await. + +This proposal aims to introduce a new Progress Reporting API —— `ProgressManager` —— to Foundation. This API is designed with several key objectives in mind: + +1. **Swift Concurrency Integration**: This API enables smooth, incremental progress reporting within async/await code patterns. + +2. **Self-Documenting Design**: The types introduced in this API clearly separate the composition from observation of progress and allow developers to make it obvious which methods report progress to clients. + +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 is 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`. + +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`. -This proposal aims to introduce an efficient, easy-to-use, less error-prone Progress Reporting API —— `ProgressReporter` —— that is compatible with async/await style concurrency to Foundation. To further support the use of this Progress Reporting API with high-level UIs, this API is also `Observable`. +6. **Type-Safe Extensibility**: This API provides a structured way to attach and propagate custom metadata alongside the standard numerical progress metrics through a type-safe property system. + +7. **Dual Use Case Support**: This API provides a model that supports both function-level progress reporting and class-level progress reporting. In the latter case, the progress type exposed can be part of more than one progress tree. ## Motivation -A progress reporting mechanism that is compatible with Swift's async/await style concurrency would be to pass a `Progress` instance as a parameter to functions or methods that report progress. The current recommended usage pattern of the existing `Progress`, as outlined in [Apple Developer Documentation](https://developer.apple.com/documentation/foundation/progress), does not fit well with async/await style concurrency. Typically, a function that aims to report progress to its callers will first return an instance of the existing `Progress`. The returned instance is then added as a child to a parent `Progress` instance. +### Reporting Progress using Existing `Progress` API + +The existing `Progress` API can be used two different ways. A method can return a `Progress`, or a class can conform to the `ProgressReporting` protocol and contain a `Progress` property. + +#### Return a `Progress` Instance from a Method + +The current recommended usage pattern of the existing `Progress`, as outlined in [Apple Developer Documentation](https://developer.apple.com/documentation/foundation/progress), does not fit well with async/await style concurrency. Typically, a function that aims to report progress to its callers will first return an instance of the existing `Progress`. The returned instance is then added as a child to a parent `Progress` instance. The only way to use the existing `Progress` API in a way that is compatible with Swift's async/await style concurrency would be to pass a `Progress` instance as a parameter to functions or methods that report progress. -In the following example, the function `chopFruits(completionHandler:)` reports progress to its caller, `makeSalad()`. +In the following example, we have a function `chopFruits(completionHandler:)` reports progress to its caller, `makeSalad()`. ```swift public func makeSalad() { let progress = Progress(totalUnitCount: 3) // parent Progress instance - let subprogress = chopFruits { result in // child Progress instance + let chopSubprogress = chopFruits { result in // child Progress instance switch result { case .success(let progress): progress.completedUnitCount += 1 @@ -90,13 +86,13 @@ public func makeSalad() { print("Fruits not chopped") } } - progress.addChild(subprogress, withPendingUnitCount: 1) + progress.addChild(chopSubprogress, withPendingUnitCount: 1) } public func chopFruits(completionHandler: @escaping (Result) -> Void) -> Progress {} ``` -When we -update this function to use async/await, the previous pattern no longer composes as expected: + +When we update this function to use async/await, the previous pattern no longer composes as expected: ```swift public func makeSalad() async { @@ -108,29 +104,29 @@ public func makeSalad() async { public func chopFruits() async -> Progress {} ``` -The previous pattern of "returning" the `Progress` instance no longer composes as expected because we are forced to await the `chopFruits()` call before returning the `Progress` instance. However, the `Progress` instance that gets returned already has its `completedUnitCount` equal to `totalUnitCount`. This defeats its purpose of showing incremental progress as the code runs to completion within the method. +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. -Additionally, while it may be possible to reuse the existing `Progress` to report progress in an `async` function by passing `Progress` as an argument to the function reporting progress, it is more error-prone, as shown below: +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: ```swift -let fruits = ["apple", "orange", "melon"] -let vegetables = ["spinach", "carrots", "celeries"] +let fruits = [Ingredient("apple"), Ingredient("orange"), Ingredient("melon")] +let vegetables = [Ingredient("spinach"), Ingredient("carrots"), Ingredient("celeries")] public func makeSalad() async { let progress = Progress(totalUnitCount: 2) - let choppingProgress = Progress() - progress.addChild(subprogress, withPendingUnitCount: 1) + let chopSubprogress = Progress() + progress.addChild(chopSubprogress, withPendingUnitCount: 1) - await chopFruits(progress: subprogress) + await chopFruits(progress: chopSubprogress) - await chopVegetables(progress: subprogress) // Author's mistake: same subprogress was reused! + await chopVegetables(progress: chopSubprogress) // Author's mistake: same subprogress was reused! } public func chopFruits(progress: Progress) async { progress.totalUnitCount = Int64(fruits.count) for fruit in fruits { - await chopItem(fruit) + await fruit.chop() progress.completedUnitCount += 1 } } @@ -138,272 +134,245 @@ public func chopFruits(progress: Progress) async { public func chopVegetables(progress: Progress) async { progress.totalUnitCount = Int64(vegetables.count) // Author's mistake: overwriting progress made in `chopFruits` on the same `progress` instance! for vegetable in vegetables { - await chopItem(vegetable) + await vegetable.chop() progress.completedUnitCount += 1 } } - -public func chopItem(_ item: Ingredient) async {} ``` -The existing `Progress` was not designed in a way that enforces the usage of `Progress` instance as a function parameter to report progress. Without a strong rule about who creates the `Progress` and who consumes it, it is easy to end up in a situation where the `Progress` is used more than once. This results in nondeterministic behavior when developers may accidentally overcomplete or overwrite a `Progress` instance. +The existing `Progress` was not designed in a way that enforces the usage of `Progress` instance as a function parameter to report progress. Without a strong rule about who creates the `Progress` and who consumes it, it is easy to end up in a situation where the `Progress` is used more than once. This results in nondeterministic behavior when developers may accidentally overcomplete or overwrite a `Progress` instance. -We introduce a new progress reporting mechanism following the new `ProgressReporter` type. This type encourages safer practices of progress reporting, separating what to be passed as parameter from what to be used to report progress. +#### Return a `Progress` Instance from a Class Conforming to `Foundation.ProgressReporting` Protocol -This proposal outlines the use of `ProgressReporter` as reporters of progress and `~Copyable` `Subprogress` as parameters passed to progress reporting methods. +Another recommended usage pattern of `Progress`, which involves the `ProgressReporting` protocol, is outlined in [Apple Developer Documentation](https://developer.apple.com/documentation/foundation/progressreporting). While this approach does not suffer from the same problem of `Progress` completing before being added to be part of a `Progress` tree, it exposes all of the mutable state of `Progress` to its observers. -## Proposed solution and example +### `ProgressManager` API -### Reporting Progress (General Operations) +We propose introducing a new progress reporting type called `ProgressManager`. `ProgressManager` is used to report progress. -To begin, let's create a framework called `SaladMaker` that contains functionalities that can make a salad and has built-in progress reporting. +In order to compose progress into trees, we also introduce two more types: -```swift -struct Fruit { - func chop() async { ... } -} +1. `Subprogress`: A `~Copyable` type, used when a `ProgressManager` wishes to assign a portion of its total progress to an `async` function. +2. `ProgressReporter`: A class used to report progress to interested observers. This includes one or more other `ProgressManager`s, which may incorporate those updates into their own progress. -struct Dressing { - func pour() async { ... } -} +```mermaid +block-beta +columns 1 -public class SaladMaker { + ProgressReporter + space + ProgressManager + space + Subprogress + + ProgressManager --> ProgressReporter + ProgressManager --> Subprogress +``` + +## Proposed solution and example + +### Reporting Progress using `Subprogress` + +To begin, let's assume there is a library `FoodProcessor` and a library `Juicer`. Both libraries report progress. + +```swift +// FoodProcessor.framework +public class FoodProcessor { + + func process(ingredients: [Ingredient], subprogress: consuming Subprogress? = nil) async { + let manager = subprogress?.manager(totalCount: ingredients.count + 1) + + // Do some work in a function + await chop(manager?.subprogress(assigningCount: ingredients.count)) + + // Call some other function that does not yet support progress reporting, and complete the work myself + await blender.blend(ingredients) + manager?.complete(count: 1) + } - let fruits: [Fruit] - let dressings: [Dressing] + static func chop(_ ingredient: Ingredient) -> Ingredient { ... } +} + +// Juicer.framework +public class Juicer { - public init() { - fruits = [Fruit("apple"), Fruit("banana"), Fruit("cherry")] - dressings = [Dressing("mayo"), Dressing("mustard"), Dressing("ketchup")] + public func makeJuice(ingredients: [Ingredient], subprogress: consuming Subprogress? = nil) async { + let manager = subprogress?.manager(totalCount: ingredients.count) + + for ingredient in ingredients { + await ingredient.blend() + manager?.complete(count: 1) + } } } ``` -In order to report progress on subparts of making a salad, such as `chopFruits` and `mixDressings`, the framework methods each has a `Subprogress` parameter. The `Subprogress` parameter is also optional to provide developers the option to either opt-in to receiving progress updates when calling each of the methods. - -Within the methods thay report progress, each `Subprogress` passed into the subparts then has to be consumed to initialize an instance of `ProgressReporter`. This is done by calling `reporter(totalCount:)` on `Subprogress`. This can be done as follows: +When we prepare dinner, we may want to use both the `FoodProcessor` and `Juicer` asynchronously. We can do so as follows: ```swift -extension SaladMaker { - - private func chopFruits(progress: consuming Subprogress?) async { - // Initialize a progress reporter to report progress on chopping fruits - // with passed-in progress parameter - let choppingReporter = progress?.reporter(totalCount: fruits.count) - for fruit in fruits { - await fruit.chop() - choppingReporter?.complete(count: 1) - } +// Developer Code +let overallManager = ProgressManager(totalCount: 2) +let foodProcessor = FoodProcessor() +let juicer = Juicer() +let mainCourse = [Ingredient("Spinach"), Ingredient("Cabbage"), Ingredient("Carrot")] +let beverage = [Ingredient("Celery"), Ingredient("Kale"), Ingredient("Apple")] + +await withTaskGroup(of: Void.self) { group in + group.addTask { + // Instantiate `Subprogress` to pass as a parameter + await foodProcessor.process(ingredients: mainCourse, subprogress: overallManager.subprogress(assigningCount: 1)) } - private func mixDressings(progress: consuming Subprogress?) async { - // Initialize a progress reporter to report progress on mixing dressing - // with passed-in progress parameter - let dressingReporter = progress?.reporter(totalCount: dressings.count) - for dressing in dressings { - await dressing.pour() - dressingReporter?.complete(count: 1) - } + group.addTask { + await juicer.makeJuice(ingredients: beverage, subprogress: overallManager.subprogress(assigningCount: 1)) } } ``` -When a developer wants to use the `SaladMaker` framework and track the progress of making a salad, they can do so as follows: +

-```swift -func makeSalad() async { - let saladMaker = SaladMaker() - - // Initialize a root-level `ProgressReporter` representing overall progress - let overall = ProgressReporter(totalCount: 100) + - // Call `chopFruits` and opt-in to receive progress updates - // by passing in a `Subprogress` constituting 70 count of overall progress - await saladMaker.chopFruits(progress: overall.subprogress(assigningCount: 70)) - - print("Chopped fruits, salad is \(overall.formatted(.fractionCompleted()))") - - // Call `mixDressings` and opt-in to receive progress updates - // by passing in a `Subprogress` constituting 30 count of overall progress - await saladMaker.mixDressings(progress: overall.subprogress(assigningCount: 30)) - - print("Mixed dressings, salad is \(overall.formatted(.fractionCompleted()))") -} +### Advantages of using `Subprogress` as Currency Type -await makeSalad() -``` + -### Reporting Progress (File-Related Operations) +There are two main advantages to using `Subprogress` as a currency type for assigning progress: -With the use of @dynamicMemberLookup attribute, `ProgressReporter` is able to access properties that are not explicitly defined in the class. This means that developers are able to define additional properties on the class specific to the operations they are reporting progress on. For instance, we pre-define additional file-related properties on `ProgressReporter` by extending `ProgressReporter` for use cases of reporting progress on file operations. +1. It is `~Copyable` to ensure that the progress is assigned to only one task. Additionally, due to its `~Copyable` nature, the API can detect whether or not the `Subprogress` is consumed. If the `Subprogress` is not converted into a `ProgressManager` (for example, due to an error or early return), then the assigned count is marked as completed in the parent `ProgressManager`. +2. When used as an argument to a function, it is clear that the function supports reporting progress. ->Note: The mechanisms of how extending `ProgressReporter` to include additional properties will be shown in the Detailed Design section of the proposal. +Here's how you use the proposed API to get the most of its benefits: -In this section, we will show an example of how we report progress with additional file-related properties: +1. Pass `Subprogress` as a parameter to functions that report progress. -To begin, let's create a class `ImageProcessor` that has the functionalities of downloading images and applying a filter onto images. +Inside the function, create a child `ProgressManager` instance to report progress in its own world via `manager(totalCount:)`, as follows: -```swift -struct Image { - let bytes: UInt64 - - func download() async { ... } - - func applyFilter() async { ... } +```swift +func correctlyUsingSubprogress() async { + let overall = ProgressManager(totalCount: 2) + await subTask(subprogress: overall.subprogress(assigningCount: 1)) } -final class ImageProcessor: Sendable { - - let images: [Image] - - init(images: [Image]) { - self.images = images +func subTask(subprogress: consuming Subprogress? = nil) async { + let count = 10 + let manager = subprogress?.manager(totalCount: count) // returns an instance of ProgressManager that can be used to report progress of subtask + for _ in 1...count { + manager?.complete(count: 1) // reports progress as usual } } ``` -The method to download images would also report information such as `totalByteCount` along with the properties directly defined on a `ProgressReporter`. While `totalByteCount` is not directly defined on the `ProgressReporter` class, we can still set the property `totalByteCount` via the `withProperties` closure because this property can be discovered at runtime via the `@dynamicMemberLookup` attribute. +If developers accidentally try to report progress to a passed-in `Subprogress`, the compiler can inform developers, as the following. The fix is quite straightforward: The only one function on `Subprogress` is `manager(totalCount:)` which creates a manager to report progress on, so the developer can easily diagnose it. -The subpart of applying filter does not contain additional file-related information, so we report progress on this subpart as usual. +```swift +func subTask(subprogress: consuming Subprogress? = nil) async { + // COMPILER ERROR: Value of type 'Subprogress' has no member 'complete' + subprogress?.complete(count: 1) +} +``` -Both the `downloadImagesFromDisk` and `applyFilterToImages` methods allow developers the option to receive progress updates while the tasks are carried out. +2. Consume each `Subprogress` only once. + +Developers should create only one `Subprogress` for a corresponding to-be-instantiated `ProgressManager` instance, as follows: ```swift -extension ImageProcessor { +func correctlyCreatingOneSubprogressForOneSubtask() { + let overall = ProgressManager(totalCount: 2) - func downloadImagesFromDisk(progress: consuming Subprogress?) async { - // Initialize a progress reporter to report progress on downloading images - // with passed-in progress parameter - let reporter = progress?.reporter(totalCount: images.count) - - // Initialize file-related properties on the reporter - reporter?.withProperties { properties in - properties.totalFileCount = images.count - properties.totalByteCount = images.map { $0.bytes }.reduce(0, +) - } - - for image in images { - await image.download() - reporter?.complete(count: 1) - // Update each file-related property - reporter?.withProperties { properties in - properties.completedFileCount += 1 - properties.completedByteCount += image.bytes - } - } - } + let subprogressOne = overall.subprogress(assigningCount: 1) // create one Subprogress + let managerOne = subprogressOne.manager(totalCount: 10) // initialize ProgressManager instance with 10 units - func applyFilterToImages(progress: consuming Subprogress?) async { - // Initializes a progress reporter to report progress on applying filter - // with passed-in progress parameter - let reporter = progress?.reporter(totalCount: images.count) - for image in images { - await image.applyFilter() - reporter?.complete(count: 1) - } - } + let subprogressTwo = overall.subprogress(assigningCount: 1) //create one Subprogress + let managerTwo = subprogressTwo.manager(totalCount: 8) // initialize ProgressManager instance with 8 units } ``` -When a developer wants to use the `ImageProcessor` framework to download images and apply filters on them, they can do so as follows: +If developer forgets to create a new `Subprogress` and try to reuse an already consumed `Subprogress`, the code will not compile: ```swift -func downloadImagesAndApplyFilter() async { - let imageProcessor = ImageProcessor(images: [Image(bytes: 1000), Image(bytes: 2000), Image(bytes: 3000)]) - - // Initialize a root-level `ProgressReporter` representing overall progress - let overall = ProgressReporter(totalCount: 2) +func incorrectlyCreatingOneSubprogressForMultipleSubtasks() { + let overall = ProgressManager(totalCount: 2) - // Call `downloadImagesFromDisk` and opt-in to receive progress updates - // by passing in a `Subprogress` constituting 1 count of overall progress - await imageProcessor.downloadImagesFromDisk(progress: overall.subprogress(assigningCount: 1)) - - // Call `applyFilterToImages` and opt-in to receive progress updates - // by passing in a `Subprogress` constituting 1 count of overall progress - await imageProcessor.applyFilterToImages(progress: overall.subprogress(assigningCount: 1)) -} + let subprogressOne = overall.subprogress(assigningCount: 1) // create one Subprogress + let managerOne = subprogressOne.manager(totalCount: 10) // initialize ProgressManager instance with 10 units -await downloadImagesAndApplyFilter() + // COMPILER ERROR: 'subprogressOne' consumed more than once + let managerTwo = subprogressOne.manager(totalCount: 8) // initialize ProgressManager instance with 8 units using same Subprogress +} ``` -### Advantages of using `Subprogress` as Currency Type +This prevents the problem of stealing or overwriting the progress as we've seen in the [Motivation section](#motivation). -The advantages of `ProgressReporter` mainly derive from the use of `Subprogress` as a currency to create descendants of `ProgresssReporter`, and the recommended ways to use `Subprogress` are as follows: +
-1. Pass `Subprogress` instead of `ProgressReporter` as a parameter to methods that report progress. +### Reporting Progress using `ProgressReporter` -`Subprogress` should be used as the currency to be passed into progress-reporting methods, within which a child `ProgressReporter` instance that constitutes a portion of its parent's total units is created via a call to `reporter(totalCount:)`, as follows: +In cases where a library wishes to report progress that may be composed into several trees, or when it reports progress that is disconnected from the result of a single function call, it can report progress with a `ProgressReporter`. An example of this is tracking days remaining till an examination: -```swift -func correctlyReportToSubprogressAfterInstantiatingReporter() async { - let overall = ProgressReporter(totalCount: 2) - await subTask(progress: overall.subprogress(assigningCount: 1)) -} - -func subTask(progress: consuming Subprogress) async { - let count = 10 - let progressReporter = progress.reporter(totalCount: count) // returns an instance of ProgressReporter - for _ in 1...count { - progressReporter?.complete(count: 1) // reports progress as usual - } +```swift +// ExamCountdown Library +public class ExamCountdown { + // Returns the progress reporter for how many days until exam + public var progressReporter: ProgressReporter } ``` -While developers may accidentally make the mistake of trying to report progress to a passed-in `Subprogress`, the fact that it does not have the same properties as an actual `ProgressReporter` means the compiler can inform developers when they are using either `ProgressReporter` or `Subprogress` wrongly. The only way for developers to kickstart actual progress reporting with `Subprogress` is by calling the `reporter(totalCount:)` to create a `ProgressReporter`, then subsequently call `complete(count:)` on `ProgressReporter`. +When we want to observe progress reported by the class `ExamCountdown`, we can observe `ProgressReporter` directly, as it is `@Observable` and contains read-only access to all properties of `ProgressManager` from which it is instantiated from, as follows: -Each time before progress reporting happens, there needs to be a call to `reporter(totalCount:)`, which returns a `ProgressReporter` instance, before calling `complete(count:)` on the returned `ProgressReporter`. +```swift +// Developer Code +let examCountdown = ExamCountdown() + +let observedProgress = examCountdown.progressReporter +``` -The following faulty example shows how reporting progress directly to `Subprogress` without initializing it will be cause a compiler error. Developers will always need to instantiate a `ProgressReporter` from `ProgresReporter.Progress` before reporting progress. +Additionally, `ProgressReporter` can also be added to be part of **more than one progress tree**, as follows: ```swift -func incorrectlyReportToSubprogressWithoutInstantiatingReporter() async { - let overall = ProgressReporter(totalCount: 2) - await subTask(progress: overall.subprogress(assigningCount: 1)) -} +// Add `ProgressReporter` to a parent `ProgressManager` +let overall = ProgressManager(totalCount: 5) +overall.assign(count: 3, to: examCountdown.progressReporter) -func subTask(progress: consuming Subprogress) async { - // COMPILER ERROR: Value of type 'Subprogress' has no member 'complete' - progress.complete(count: 1) -} -``` +// Add `ProgressReporter` to another parent `ProgressManager` with different assigned count +let deadlineTracker = ProgressManager(totalCount: 2) +overall.assign(count: 1, to: examCountdown, progressReporter) + +``` + +### Reporting Progress With Type-Safe Custom Properties -2. Consume each `Subprogress` only once, and if not consumed, its parent `ProgressReporter` behaves as if none of its units were ever allocated to create `Subprogress`. +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. -Developers should create only one `Subprogress` for a corresponding to-be-instantiated `ProgressReporter` instance, as follows: +We can declare a custom additional property as follows: ```swift -func correctlyConsumingSubprogress() { - let overall = ProgressReporter(totalCount: 2) +struct Filename: ProgressManager.Property { + typealias Value = String - let progressOne = overall.subprogress(assigningCount: 1) // create one Subprogress - let reporterOne = progressOne.reporter(totalCount: 10) // initialize ProgressReporter instance with 10 units - - let progressTwo = overall.subprogress(assigningCount: 1) //create one Subprogress - let reporterTwo = progressTwo.reporter(totalCount: 8) // initialize ProgressReporter instance with 8 units + static var defaultValue: String { "" } } -``` -It is impossible for developers to accidentally consume `Subprogress` more than once, because even if developers accidentally **type** out an expression to consume an already-consumed `Subprogress`, their code won't compile at all. +extension ProgressManager.Properties { + var filename: Filename.Type { Filename.self } +} +``` -The `reporter(totalCount:)` method, which **consumes** the `Subprogress`, can only be called once on each `Subprogress` instance. If there are more than one attempts to call `reporter(totalCount:)` on the same instance of `Subprogress`, the code will not compile due to the `~Copyable` nature of `Subprogress`. +You can report custom properties using `ProgressManager` as follows: ```swift -func incorrectlyConsumingSubprogress() { - let overall = ProgressReporter(totalCount: 2) - - let progressOne = overall.subprogress(assigningCount: 1) // create one Subprogress - let reporterOne = progressOne.reporter(totalCount: 10) // initialize ProgressReporter instance with 10 units - - // COMPILER ERROR: 'progressOne' consumed more than once - let reporterTwo = progressOne.reporter(totalCount: 8) // initialize ProgressReporter instance with 8 units using same Progress +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 } ``` ### Interoperability with Existing `Progress` -In both cases below, the propagation of progress of subparts to a root progress should work the same ways the existing `Progress` and `ProgressReporter` work. +In both cases below, the propagation of progress of subparts to a root progress should work the same ways as the existing `Progress`. Due to the fact that the existing `Progress` assumes a tree structure instead of an acyclic graph structure of the new `ProgressManager`, interoperability between `Progress` and `ProgressManager` similarly assumes the tree structure. -Consider two progress reporting methods, one which utilizes the existing `Progress`, and another using `ProgressReporter`: +Consider two progress reporting methods, one of which utilizes the existing `Progress`, and the other `ProgressManager`: ```swift // Framework code: Function reporting progress with the existing `Progress` @@ -418,69 +387,64 @@ func doSomethingWithProgress() -> Progress { return p } -// Framework code: Function reporting progress with `ProgressReporter` -func doSomethingWithReporter(progress: consuming Subprogress) async -> Int { - let reporter = progress.reporter(totalCount: 2) +// Framework code: Function reporting progress with `Subprogress` +func doSomethingWithManager(subprogress: consuming Subprogress) async -> Int { + let manager = subprogress.manager(totalCount: 2) //do something - reporter.complete(count: 1) + manager.complete(count: 1) //do something - reporter.complete(count: 1) + manager.complete(count: 1) +} + +// Framework code: Library reporting progress with `ProgressReporter` +class DownloadManager { + var progressReporter: ProgressReporter { get } } ``` -In the case in which we need to receive a `Progress` instance and add it as a child to a `ProgressReporter` parent, we can use the interop method `subprogress(assigningCount: to:)`. +In the case in which we need to receive a `Progress` instance and add it as a child to a `ProgressManager` parent, we can use the interop method `subprogress(assigningCount: to:)`. -The choice of naming the interop method as `subprogress(assigningCount: to:)` is to keep the syntax consistent with the method used to add a `ProgressReporter` instance to the progress tree, `subprogress(assigningCount:)`. An example of how these can be used to compose a `ProgressReporter` tree with a top-level `ProgressReporter` is as follows: +An example of how this can be used to compose a `ProgressManager` tree with a top-level `ProgressManager` is as follows: ```swift // Developer code -func reporterParentProgressChildInterop() async { - let overall = ProgressReporter(totalCount: 2) // Top-level `ProgressReporter` - - // Assigning 1 unit of overall's `totalCount` to `Subprogress` - let progressOne = overall.subprogress(assigningCount: 1) - // Passing `Subprogress` to method reporting progress - let result = await doSomethingWithReporter(progress: progressOne) - - - // Getting a `Progress` from method reporting progress - let progressTwo = doSomethingWithProgress() - // Assigning 1 unit of overall's `totalCount` to the existing `Progress` - overall.subprogress(assigningCount: 1, to: progressTwo) -} +let overall = ProgressManager(totalCount: 2) // Top-level `ProgressManager` + +let subprogressOne = overall.subprogress(assigningCount: 1) +let result = await doSomethingWithManager(subprogress: subprogressOne) + +let subprogressTwo = doSomethingWithProgress() +overall.subprogress(assigningCount: 1, to: subprogressTwo) ``` -The reverse case, in which a framework needs to receive a `ProgressReporter` instance as a child from a top-level `Progress`, can also be done. The interop method `makeChild(withPendingUnitCount: kind:)` added to `Progress` will support the explicit composition of a progress tree. +The reverse case, in which a `ProgressManager` instance needs to become a child of a top-level `Progress` via `Subprogress` or `ProgressReporter`, can also be done. We can use the methods `makeChild(withPendingUnitCount:)` for getting a `Subprogress` and `addChild(_:withPendingUnitCount:)` for adding a `ProgressReporter` as a child. -The choice of naming the interop method as `makeChild(withPendingUnitCount: kind:)` is to keep the syntax consistent with the method used to add a `Foundation.Progress` instance as a child, `addChild(_: withPendingUnitCount:)`. An example of how this can be used to compose a `Foundation.Progress` tree with a top-level `Foundation.Progress` is as follows: +An example of how this can be used to compose a `Foundation.Progress` tree with a top-level `Foundation.Progress` is as follows: ```swift // Developer code -func progressParentReporterChildInterop() { - let overall = Progress(totalUnitCount: 2) // Top-level `Progress` - - // Getting a `Progress` from method reporting progress - let progressOne = doSomethingWithProgress() - // Add Foundation's `Progress` as a child which takes up 1 unit of overall's `totalUnitCount` - overall.addChild(progressOne, withPendingUnitCount: 1) - - // Getting a `Subprogress` which takes up 1 unit of overall's `totalUnitCount` - let progressTwo = overall.makeChild(withPendingUnitCount: 1) - // Passing `Subprogress` instance to method reporting progress - doSomethingWithReporter(progress: progressTwo) -} +let overall = Progress(totalUnitCount: 3) // Top-level `Progress` + +let subprogressOne = doSomethingWithProgress() +overall.addChild(subprogressOne, withPendingUnitCount: 1) + +let subprogressTwo = overall.makeChild(withPendingUnitCount: 1) +doSomethingWithManager(subprogress: subprogressTwo) + +let subprogressThree = DownloadManager().progressReporter +overall.addChild(subprogressThree, withPendingUnitCount: 1) ``` ## Detailed design -### `ProgressReporter` +### `ProgressManager` -`ProgressReporter` is an Observable and Sendable class that developers use to report progress. Specifically, an instance of `ProgressReporter` can be used to either track progress of a single task, or track progress of a tree of `ProgressReporter` instances. +`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. ```swift /// An object that conveys ongoing progress to the user for a specified task. @available(FoundationPreview 6.2, *) -@Observable public final class ProgressReporter : Sendable, Hashable, Equatable, CustomDebugStringConvertible { +@Observable public final class ProgressManager : Sendable, Hashable, Equatable, CustomDebugStringConvertible { /// The total units of work. public var totalCount: Int? { get } @@ -502,22 +466,23 @@ func progressParentReporterChildInterop() { /// If `completedCount` >= `totalCount`, the value will be `true`. public var isFinished: Bool { get } + /// A `ProgressReporter` instance, used for providing read-only observation of progress updates or composing into other `ProgressManager`s. + public var reporter: ProgressReporter { get } + + /// A debug description. + public var debugDescription: String { get } + /// A type that conveys additional task-specific information on progress. public protocol Property { - associatedtype T : Sendable + associatedtype Value : Sendable /// The default value to return when property is not set to a specific value. - static var defaultValue: T { get } - - /// Aggregates an array of `T` into a single value `T`. - /// - Parameter all: Array of `T` to be aggregated. - /// - Returns: A new instance of `T`. - static func reduce(_ all: [T]) -> T + static var defaultValue: Value { get } } /// A container that holds values for properties that convey information about progress. - @dynamicMemberLookup public struct Values : Sendable, CustomStringDebugConvertible { + @dynamicMemberLookup public struct Values : Sendable { /// The total units of work. public var totalCount: Int? { mutating get set } @@ -526,10 +491,7 @@ func progressParentReporterChildInterop() { 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) -> P.T where P : ProgressReporter.Property { get set } - - /// Returns a debug description. - public static var debugDescription: String { get } + public subscript(dynamicMember key: KeyPath) -> Property.Value { get set } } /// Initializes `self` with `totalCount`. @@ -538,48 +500,103 @@ func progressParentReporterChildInterop() { /// - Parameter totalCount: Total units of work. public convenience init(totalCount: Int?) - /// Returns a `Subprogress` representing a portion of `self`which can be passed to any method that reports progress. + /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. + /// + /// If the `Subprogress` is not converted into a `ProgressManager` (for example, due to an error or early return), + /// then the assigned count is marked as completed in the parent `ProgressManager`. /// - /// - Parameter count: Units, which is a portion of `totalCount` delegated to an instance of `Subprogress`. + /// - Parameter count: The portion of `totalCount` to be delegated to the `Subprogress`. /// - Returns: A `Subprogress` instance. - public func subprogress(assigningCount portionOfParent: Int) -> Subprogress + public func subprogress(assigningCount count: Int) -> Subprogress + + /// Adds a `ProgressReporter` as a child, with its progress representing a portion of `self`'s progress. + /// + /// - Parameters: + /// - output: A `ProgressReporter` instance. + /// - count: The portion of `totalCount` to be delegated to the `ProgressReporter`. + public func assign(count: Int, to reporter: ProgressReporter) /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) /// Accesses or mutates any properties that convey additional information about progress. - public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T + public func withProperties( + _ closure: (inout sending Values) throws(E) -> sending T + ) throws(E) -> sending T + + /// 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?] + + /// 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 +} +``` + +### `Subprogress` + +You call `ProgressManager`'s `subprogress(assigningCount:)` to create a `Subprogress`. It is a `~Copyable` instance that you pass into functions that report progress. - /// Returns a debug description. - public static var debugDescription: String { get } +The callee will consume `Subprogress` and get the `ProgressManager` by calling `manager(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 `manager(totalCount:)` on a Subprogress. +public struct Subprogress: ~Copyable, Sendable { + + /// Instantiates a ProgressManager which is a child to the parent ProgressManager from which the Subprogress is created. + /// + /// - Parameter totalCount: Total count of returned child `ProgressManager` instance. + /// - Returns: A `ProgressManager` instance. + public consuming func manager(totalCount: Int?) -> ProgressManager } +``` + +### `ProgressReporter` -/// Default implementation for `reduce` where T is `AdditiveArithmetic`. +```swift @available(FoundationPreview 6.2, *) -extension ProgressReporter.Property where Self.T : AdditiveArithmetic { - /// Aggregates an array of `T` into a single value `T`. - /// - /// All `T` `AdditiveArithmetic` values are added together. - /// - Parameter all: Array of `T` to be aggregated. - /// - Returns: A new instance of `T`. - public static func reduce(_ all: [T]) -> T +/// 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 { + + public var totalCount: Int? { get } + + public var completedCount: Int { get } + + public var fractionCompleted: Double { get } + + public var isIndeterminate: Bool { get } + + public var isFinished: Bool { get } + + public func withProperties(_ closure: @Sendable (ProgressManager.Values) throws(E) -> T) throws(E) -> T } ``` -### `ProgressReporter.Properties` +### `ProgressManager.Properties` -`ProgressReporter.Properties` is a struct that contains declarations of additional properties that are not defined directly on `ProgressReporter`, but discovered at runtime via `@dynamicMemberLookup`. These additional properties should be defined separately in `ProgressReporter` because neither are they used to drive forward progress like `totalCount` and `completedCount`, nor are they applicable in all cases of progress reporting. +`ProgressManager.Properties` is a struct that contains declarations of additional properties that are not defined directly on `ProgressManager`, but discovered at runtime via `@dynamicMemberLookup`. These additional properties should be defined separately in `ProgressManager` because neither are they used to drive forward progress like `totalCount` and `completedCount`, nor are they applicable in all cases of progress reporting. -We pre-declare some of these additional properties that are commonly desired in use cases of progress reporting such as `totalFileCount` and `totalByteCount`. +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`. -For developers that would like to report additional metadata or properties as they use `ProgressReporter` to report progress, they will need to add declarations of their additional properties into `ProgressReporter.Properties`, similar to how the pre-declared additional properties are declared. +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. ```swift @available(FoundationPreview 6.2, *) -extension ProgressReporter { - -extension ProgressReporter { +extension ProgressManager { public struct Properties { @@ -588,7 +605,7 @@ extension ProgressReporter { public struct TotalFileCount : Property { - public typealias T = Int + public typealias Value = Int public static var defaultValue: Int { get } } @@ -598,7 +615,7 @@ extension ProgressReporter { public struct CompletedFileCount : Property { - public typealias T = Int + public typealias Value = Int public static var defaultValue: Int { get } } @@ -608,7 +625,7 @@ extension ProgressReporter { public struct TotalByteCount : Property { - public typealias T = UInt64 + public typealias Value = UInt64 public static var defaultValue: UInt64 { get } } @@ -618,7 +635,7 @@ extension ProgressReporter { public struct CompletedByteCount : Property { - public typealias T = UInt64 + public typealias Value = UInt64 public static var defaultValue: UInt64 { get } } @@ -628,7 +645,7 @@ extension ProgressReporter { public struct Throughput : Property { - public typealias T = UInt64 + public typealias Value = UInt64 public static var defaultValue: UInt64 { get } } @@ -638,7 +655,7 @@ extension ProgressReporter { public struct EstimatedTimeRemaining : Property { - public typealias T = Duration + public typealias Value = Duration public static var defaultValue: Duration { get } } @@ -646,158 +663,79 @@ extension ProgressReporter { } ``` -### `Subprogress` +### Cancellation in `ProgressManager` -An instance of `Subprogress` is returned from a call to `ProgressReporter`'s `subprogress(assigningCount:)`. `Subprogress` acts as an intermediary instance that you pass into functions that report progress. Additionally, callers should convert `Subprogress` to `ProgressReporter` before starting to report progress with it by calling `reporter(totalCount:)`. +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. -```swift -@available(FoundationPreview 6.2, *) -extension ProgressReporter { +While there are many different ways to handle cancellation, as discussed in the Alternatives Considered section [here](#introduce-explicit-support-for-cancellation\,-pausing\,-and-resuming-of-this-progress-reporting-api) and [here](#handling-cancellation-by-checking-task-cancellation-or-allowing-incomplete-progress-after-task-cancellation) we have decided that the way `ProgressManager` handles cancellation would be to complete the `ProgressManager` by setting its `completedCount` to `totalCount`, thus ensuring that the `fractionCompleted` still progress towards 1.00. Similarly, a `Subprogress` that is created but not consumed will also be completed. - public struct Progress : ~Copyable, Sendable { - - /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. - /// - Parameter totalCount: Total count of returned child `ProgressReporter` instance. - /// - Returns: A `ProgressReporter` instance. - public consuming func reporter(totalCount: Int?) -> ProgressReporter - } -} -``` +In cases where you encounter cancellation, and would like to present more information about cancellation, you can create a custom property to report that information to clients. -### `ProgressReporter.FormatStyle` +### Getting `AsyncStream` via Observation -`ProgressReporter.FormatStyle` is used to configure the formatting of `ProgressReporter` into localized descriptions. You can specify which option to format `ProgressReporter` with, and call the `format(_:)` method to get a localized string containing information that you have specified when initializing a `ProgressReporter.FormatStyle`. - -```swift -@available(FoundationPreview 6.2, *) -extension ProgressReporter { - - public struct FormatStyle : Codable, Equatable, Hashable { - - public struct Option : Codable, Hashable, Equatable { - - /// Option specifying `fractionCompleted`. - /// - /// For example, 20% completed. - /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. - /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - public static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent()) -> Option - - /// Option specifying `completedCount` / `totalCount`. - /// - /// For example, 5 of 10. - /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. - /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - public static func count(format style: IntegerFormatStyle = IntegerFormatStyle()) -> Option - } - - public var locale: Locale - - public init(_ option: Option, locale: Locale = .autoupdatingCurrent) - } -} - -@available(FoundationPreview 6.2, *) -extension ProgressReporter.FormatStyle : FormatStyle { - - public func locale(_ locale: Locale) -> ProgressReporter.FormatStyle - - public func format(_ reporter: ProgressReporter) -> String -} -``` - -To provide convenience methods for formatting `ProgressReporter`, we also provide the `formatted(_:)` method that developers can call on any `ProgressReporter`. - -```swift -@available(FoundationPreview 6.2, *) -extension ProgressReporter { - - public func formatted(_ style: F) -> F.FormatOutput where F : FormatStyle, F.FormatInput == ProgressReporter -} - -@available(FoundationPreview 6.2, *) -extension FormatStyle where Self == ProgressReporter.FormatStyle { - - public static func fractionCompleted(format: FloatingPointFormatStyle.Percent) -> Self - - public static func count(format: IntegerFormatStyle) -> Self -} -``` - -### `ProgressReporter.FileFormatStyle` - -The custom format style for additional file-related properties are also implemented as follows: +`ProgressManager` and `ProgressReporter` are both `Observable` final classes. As proposed in [SE-0475: Transactional Observation of Values](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0475-observed.md), there will be a way for developers to get an `AsyncSequence` to observe changes to instances that are `Observable`. We can obtain `AsyncStream` for `ProgressManager` and `ProgressReporter` as follows: ```swift -@available(FoundationPreview 6.2, *) -extension ProgressReporter { - - public struct FileFormatStyle : Codable, Equatable, Hashable { - - public struct Options : Codable, Equatable, Hashable { - - /// Option specifying all file-related properties. - public static var file: Option { get } - } - - public var locale: Locale - - public init(_ option: Options, locale: Locale = .autoupdatingCurrent) - } -} - -@available(FoundationPreview 6.2, *) -extension ProgressReporter.FileFormatStyle : FormatStyle { +let manager = ProgressManager(totalCount: 2) +let managerFractionStream = Observations { manager.fractionCompleted } - public func locale(_ locale: Locale) -> ProgressReporter.FileFormatStyle +let reporter = manager.reporter +let reporterFractionStream = Observations { reporter.fractionCompleted } +``` - public func format(_ reporter: ProgressReporter) -> String -} +### Methods for Interoperability with Existing `Progress` -@available(FoundationPreview 6.2, *) -extension FormatStyle where Self == ProgressReporter.FileFormatStyle { +> In line with the structure of tree-based progress reporting in the existing `Progress`, interoperability support between `NSProgress` and `ProgressManager` assumes that developers will only construct progress trees with single parents using these interoperability methods. - public static var file: Self { get } -} -``` +To allow frameworks which may have dependencies on the pre-existing progress-reporting protocol to adopt this new progress-reporting protocol, either as a recipient of a child `Progress` instance that needs to be added to its `ProgressManager` tree, or as a provider of `ProgressManager` that may later be added to another framework's `Progress` tree, there needs to be additional support for ensuring that progress trees can be composed with in three cases: -### Methods for Interoperability with Existing `Progress` +1. A `ProgressManager` is a parent to a `Foundation.Progress` child +2. A `Foundation.Progress` is a parent to a `ProgressManager` child -To allow frameworks which may have dependencies on the pre-existing progress-reporting protocol to adopt this new progress-reporting protocol, either as a recipient of a child `Progress` instance that needs to be added to its `ProgressReporter` tree, or as a provider of `ProgressReporter` that may later be added to another framework's `Progress` tree, there needs to be additional support for ensuring that progress trees can be composed with in two cases: -1. A `ProgressReporter` instance has to parent a `Progress` child -2. A `Progress` instance has to parent a `ProgressReporter` child +#### `ProgressManager` (Parent) - `Foundation.Progress` (Child) -#### ProgressReporter (Parent) - 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 `Progress` as a child to an instance of `ProgressReporter`, we pass an `Int` for the portion of `ProgressReporter`'s `totalCount` `Progress` should take up and a `Progress` instance to `subprogress(assigningCount: to:)`. The `ProgressReporter` instance will track the `Progress` instance just like any of its `ProgressReporter` 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, *) -extension ProgressReporter { - // Adds a `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. +extension ProgressManager { + /// Adds a Foundation's `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. + /// /// - Parameters: /// - count: Number of units delegated from `self`'s `totalCount`. - /// - progress: `Progress` which receives the delegated `count`. - public func subprogress(assigningCount: Int, to progress: Foundation.Progress) + /// - progress: `Foundation.Progress` which receives the delegated `count`. + public func subprogress(assigningCount count: Int, to progress: Foundation.Progress) } ``` -#### Progress (Parent) - ProgressReporter (Child) +#### `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 `ProgressReporter` as a child to an instance of the existing `Progress`, the `Progress` instance calls `makeChild(count:kind:)` to get a `Subprogress` instance that can be passed as a parameter to a function that reports progress. The `Progress` instance will track the `ProgressReporter` 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, *) extension Progress { /// Returns a Subprogress which can be passed to any method that reports progress - /// and can be initialized into a child `ProgressReporter` to the `self`. + /// and can be initialized into a child `ProgressManager` to the `self`. /// - /// Delegates a portion of totalUnitCount to a future child `ProgressReporter` instance. + /// Delegates a portion of totalUnitCount to a future child `ProgressManager` instance. /// - /// - Parameter count: Number of units delegated to a child instance of `ProgressReporter` - /// which may be instantiated by `Subprogress` later when `reporter(totalCount:)` is called. + /// - Parameter count: Number of units delegated to a child instance of `ProgressManager` + /// which may be instantiated by `Subprogress` later when `manager(totalCount:)` is called. /// - Returns: A `Subprogress` instance. - public func makeChild(withPendingUnitCount count: Int) -> Subprogress + public func makeChild(withPendingUnitCount count: Int) -> Subprogress + + + /// Adds a ProgressReporter as a child to a Foundation.Progress. + /// + /// - Parameters: + /// - output: A `ProgressReporter` instance. + /// - count: Number of units delegated from `self`'s `totalCount` to Progress Reporter. + public func addChild(_ reporter: ProgressReporter, withPendingUnitCount count: Int) } ``` @@ -805,88 +743,91 @@ extension Progress { There should be no impact on existing code, as this is an additive change. -However, this new progress reporting API, `ProgressReporter`, which is compatible with Swift's async/await style concurrency, will be favored over the existing `Progress` API going forward. Depending on how widespread the adoption of `ProgressReporter` is, we may consider deprecating the existing `Progress` API. +However, this new progress reporting API, `ProgressManager`, which is compatible with Swift's async/await style concurrency, will be favored over the existing `Progress` API going forward. Depending on how widespread the adoption of `ProgressManager` is, we may consider deprecating the existing `Progress` API. + +## Future Directions -## Future Directions +### Introduce `FormatStyle` +We can introduce `FormatStyle` for both the `ProgressManager` and `ProgressReporter` to enable easier formatting of these types. ### Additional Overloads to APIs within UI Frameworks -To enable wider adoption of `ProgressReporter`, we can add overloads to APIs within UI frameworks that has been using Foundation's `Progress`, such as `ProgressView` in SwiftUI. Adding support to existing progress-related APIs within UI Frameworks will enable adoption of `ProgressReporter` for app developers who wish to do extensive progress reporting and show progress on the User Interface using `ProgressReporter`. +To enable wider adoption of `ProgressManager`, we can add overloads to APIs within UI frameworks that have been using Foundation's `Progress`, such as `ProgressView` in SwiftUI. Adding support to existing progress-related APIs within UI Frameworks will enable adoption of `ProgressManager` for app developers who wish to do extensive progress reporting and show progress on the User Interface using `ProgressManager`. -### Distributed `ProgressReporter` -To enable inter-process progress reporting, we would like to introduce distributed `ProgressReporter` in the future, which would functionally be similar to how Foundation's `Progress` mechanism for reporting progress across processes. +### Distributed `ProgressManager` +To enable inter-process progress reporting, we can introduce distributed `ProgressManager` in the future, which would functionally be similar to how `Progress` reports progress across processes. -### Enhanced `FormatStyle` -To enable more customization of `ProgressReporter`, we would like to introduce more options in `ProgressReporter`'s `FormatStyle`. +### Automatic Count Management for Simplified Progress Tracking +To further safeguard developers from making mistakes of over-assigning or under-assigning counts from one `ProgressManager` to another, we can consider introducing some convenience, for example, macros, to automatically manage the aggregation of units. This would be useful in scenarios in which developers have nested progress components and manual count maintenance becomes more complex. + +### Support for Non-Integer Formats of Progress Updates +To handle progress values from other sources that provide progress updates as non-integer formats such as `Double`, we can introduce a way for `ProgressManager` to either be instantiated with non-integer formats, or a peer instance of `ProgressManager` that works with `ProgressManager` to compose a progress graph. ## Alternatives considered ### Alternative Names As the existing `Progress` already exists, we had to come up with a name other than `Progress` for this API, but one that still conveys the progress-reporting functionality of this API. Some of the names we have considered are as follows: -1. Alternative to `ProgressReporter` +1. Alternative to `ProgressManager` - `AsyncProgress` + - `ProgressReporter` -We decided to proceed with the name `ProgressReporter` because prefixing an API with the term `Async` may be confusing for developers, as there is a precedent of APIs doing so, such as `AsyncSequence` adding asynchronicity to `Sequence`, whereas this is a different case for `ProgressReporter` vs `Progress`. +We ended up with `ProgressManager` because it correctly associates the API as being something that can do more than reporting progress, and it can give out a portion of its `totalCount` to report subtasks. We did not choose `AsyncProgress` because prefixing an API with the term `Async` connotes to adding asynchronicity to the API, as there is a precedent of APIs doing so, such as `AsyncSequence` adding asynchronicity to `Sequence`. `ProgressReporter` also does not appropriately convey the fact that the API can be used to construct a progress tree and that it seems to be something that is read-only, which can appear confusing to others. 2. Alternative to `Subprogress` - `ProgressReporter.Link` - `ProgressReporter.Child` - - `ProgressReporter.Token` - - `ProgressReporter.Progress` + - `ProgressReporter.Token` + - `ProgressReporter.Progress` + - `ProgressInput` -While the names `Link`, `Child`, and `Token` may appeal to the fact that this is a type that is separate from the `ProgressReporter` itself and should only be used as a function parameter and to be consumed immediately to kickstart progress reporting, it is ambiguous because developers may not immedidately figure out its function from just the name itself. While `Progress` may be a good name to indicate to developers that any method receiving `Progress` as a parameter reports progress, it is does not accurately convey its nature of being the bearer of a certain portion of some parent's `totalCount`. We landed at `Subprogress` as it serves as an indicator for developers that methods with a `Subprogress` parameter reports progress, and at the same time conveys the correct idea that it is meant to be a part of a progress tree. - -3. Alternative to `subprogress(assigningCount:)` - - `assign(count:)` +While the names `Link`, `Child`, `Token` and `Progress` may appeal to the fact that this type should only be used as a function parameter in methods that report progress and to be consumed immediately to kickstart progress reporting, it is ambiguous because developers may not immediately figure out its functionality from just the name itself. `Subprogress` is an intuitive name because developers will instinctively think of it as something that reports on subtasks and can be composed as part of a progress graph. -We initially considered naming the method that returns a `Subprogress` instance `assign(count:)` due to its nature of being a peer method to `complete(count:)`. However, `assign` does not intuitively indicate to developers that this method is supposed to return anything, so we decided on naming the method `subprogress` and its argument `assigningCount` to indicate that it is assigning a portion of its own `totalCount` to a `Subprogress` instance. +3. Alternative to `ProgressReporter` + - `ProgressOutput` -### Introduce `ProgressReporter` to Swift standard library -In consideration for making `ProgressReporter` a lightweight API for server-side developers to use without importing the entire `Foundation` framework, we considered either introducing `ProgressReporter` in a standalone module, or including `ProgressReporter` in existing Swift standard library modules such as `Observation` or `Concurrency`. However, given the fact that `ProgressReporter` has dependencies in `Observation` and `Concurrency` modules, and that the goal is to eventually support progress reporting over XPC connections, `Foundation` framework is the most ideal place to host the `ProgressReporter` as it is the central framework for APIs that provide core functionalities when these functionalities are not provided by Swift standard library and its modules. +We decided to use the name `ProgressReporter` for the currency type that can either be used to observe progress or added as a child to another `ProgressManager`. The phrase `reporter` is also suggestive of the fact that `ProgressReporter` is a type that contains read-only properties relevant to progress reporting such as `totalCount` and `completedCount`. -### Implement `ProgressReporter` as a Generic Class -In Version 1 of this proposal, we proposed implementing `ProgressReporter` as a generic class, which has a type parameter `Properties`, which conforms to the protocol `ProgressProperties`. In this case, the API reads as `ProgressReporter`. This was implemented as such to account for additional properties required in different use cases of progress reporting. For instance, `FileProgressProperties` is a type of `ProgressProperties` that holds references to properties related to file operations such as `totalByteCount` and `totalFileCount`. The `ProgressReporter` class itself will then have a `properties` property, which holds a reference to its `Properties` struct, in order to access additional properties via dot syntax, which would read as `reporter.properties.totalByteCount`. In this implementation, the typealiases introduced are as follows: - - ```swift - public typealias BasicProgressReporter = ProgressReporter - public typealias FileProgressReporter = ProgressReporter - public typealias FileProgress = ProgressReporter.Progress - public typealias BasicProgress = ProgressReporter.Progress - ``` - -However, while this provides flexibility for developers to create any custom types of `ProgressReporter`, some issues that arise include the additional properties of a child `ProgressReporter` being inaccessible by its parent `ProgressReporter` if they were not of the same type. For instance, if the child is a `FileProgressReporter` while the parent is a `BasicProgressReporter`, the parent does not have access to the child's `FileProgressProperties` because it only has reference to its own `BasicProgressProperties`. This means that developers would not be able to display additional file-related properties reported by its child in its localized descriptions without an extra step of adding a layer of children to parent different types of children in the progress reporter tree. +### Introduce this Progress Reporting API to Swift standard library +In consideration for making `ProgressManager` a lightweight API for server-side developers to use without importing the entire `Foundation` framework, we considered either introducing `ProgressManager` in a standalone module, or including `ProgressManager` in existing Swift standard library modules such as `Observation` or `Concurrency`. However, given the fact that `ProgressManager` has dependencies in `Observation` and `Concurrency` modules, and that the goal is to eventually support progress reporting over distributed actors, `Foundation` framework is the most ideal place to host the `ProgressReporter` as it is the central framework for APIs that provide core functionalities when these functionalities are not provided by Swift standard library and its modules. -We decided to replace the generic class implementation with `@dynamicMemberLookup`, making the `ProgressReporter` class non-generic, and instead relies on `@dynamicMemberLookup` to access additional properties that developers may want to use in progress reporting. This allows `ProgressReporter` to all be of the same `Type`, and at the same time retains the benefits of being able to report progress with additional properties such as `totalByteCount` and `totalFileCount`. With all progress reporters in a tree being the same type, a top-level `ProgressReporter` can access any additional properties reported by its children `ProgressReporter` without much trouble as compared to if `ProgressReporter` were to be a generic class. +### Concurrency-Integrated Progress Reporting via TaskLocal Storage +This allows a progress object to be stored in Swift `TaskLocal` storage. This allows the implicit model of building a progress tree to be used from Swift Concurrency asynchronous contexts. In this solution, `+(NSProgress *)currentProgress` and `- (void)_addImplicitChild:(NSProgress *) child` reads from TaskLocal storage when called from a Swift Concurrency context. This method was found to be not preferable as we would like to encourage the usage of the explicit model of Progress Reporting, in which we do not depend on an implicit TaskLocal storage, and for methods to be explicit about progress reporting. -### Implement `ProgressReporter` as an actor -We considered implementing `ProgressReporter` as we want to maintain this API as a reference type that is safe to use in concurrent environments. However, if `ProgressReporter` were to be implemented, `ProgressReporter` will not be able to conform to `Observable` because actor-based keypaths do not exist as of now. Ensuring that `ProgressReporter` is `Observable` is important to us, as we want to ensure that `ProgressReporter` works well with UI components in SwiftUI. +Progress being implicit is risky for evolution of source code. When a library that developers depend on introduce new functionalities later on, developers may not be aware of the progress reporting behavior change if progress is implicit, as shown below: -### Implement `ProgressReporter` as a protocol -In consideration of making the surface of the API simpler without the use of generics, we considered implementing `ProgressReporter` as a protocol, and provide implementations for specialized `ProgressReporter` classes that conform to the protocol, namely `BasicProgress`(`ProgressReporter` for progress reporting with only simple `count`) and `FileProgress` (`ProgressReporter` for progress reporting with file-related additional properties such as `totalFileCount`). This had the benefit of developers having to initialize a `ProgressReporter` instance with `BasicProgress(totalCount: 10)` instead of `ProgressReporter(totalCount: 10)`. - -However, one of the downside of this is that every time a developer wants to create a `ProgressReporter` that contains additional properties that are tailored to their use case, they would have to write an entire class that conforms to the `ProgressReporter` protocol from scratch, including the calculations of `fractionCompleted` for `ProgressReporter` trees. Additionally, the `~Copyable` struct nested within the `ProgressReporter` class that should be used as function parameter passed to functions that report progress will have to be included in the `ProgressReporter` protocol as an `associatedtype` that is `~Copyable`. However, the Swift compiler currently cannot suppress 'Copyable' requirement of an associated type and developers will need to consciously work around this. These create a lot of overload for developers wishing to report progress with additional metadata beyond what we provide in `BasicProgress` and `FileProgress` in this case. - -### Introduce an `Observable` adapter for `ProgressReporter` -We thought about introducing a clearer separation of responsibility between the reporting and observing of a `ProgressReporter`, because progress reporting is often done by the framework, and the caller of a certain method of a framework would merely observe the `ProgressReporter` within the framework. This will deter observers from accidentally mutating values of a framework's `ProgressReporter`. - -However, this means that `ProgressReporter` needs to be passed into the `Observable` adapter to make an instance `ObservableProgressReporter`, which can then be passed into `ProgressView()` later. We decided that this is too much overhead for developers to use for the benefit of avoiding observers from mutating values of `ProgressReporter`. +```swift +// initial code -### Introduce Method to Generate Localized Description -We considered introducing a `localizedDescription(including:)` method, which returns a `LocalizedStringResource` for observers to get custom format descriptions for `ProgressReporter`. In contrast, using a `FormatStyle` aligns more closely with Swift's API, and has more flexibility for developers to add custom `FormatStyle` to display localized descriptions for additional properties they may want to declare and use. +// Library code +func g() async { + // implicitly consumes task local progress +} -### Introduce Explicit Support for Cancellation, Pausing, and Resuming of `ProgressReporter` -The existing `Progress` provides support for cancelling, pausing and resuming an ongoing operation tracked by an instance of `Progress`, and propagates these actions down to all of its children. We decided to not introduce support for this behavior as there is support in cancelling a `Task` via `Task.cancel()` in Swift structured concurrency. The absence of support for cancellation, pausing and resuming in `ProgressReporter` helps to clarify the scope of responsibility of this API, which is to report progress, instead of owning a task and performing actions on it. +// App code +func f() async { + var progressManager = ProgressManager(totalUnitCount: 1) + await g() // progress consumed +} -### Check Task Cancellation within `complete(count:)` Method -We considered adding a `Task.isCancelled` check in the `complete(count:)` method so that calls to `complete(count:)` from a `Task` that is cancelled becomes a no-op. This means that once a Task is cancelled, calls to `complete(count:)` from within the task does not make any further incremental progress. +// later changed code -We decided to remove this check to transfer the responsibility back to the developer to not report progress further from within a cancelled task. Typically, developers complete some expensive async work and subsequently updates the `completedCount` of a `ProgressReporter` by calling `complete(count:)`. Checking `Task.isCancelled` means that we take care of the cancellation by not making any further incremental progress, but developers are still responsible for the making sure that they do not execute any of the expensive async work. Removing the `Task.isCancelled` check from `complete(count:)` helps to make clear that developers will be responsible for both canceling any expensive async work and any further update to `completedCount` of `ProgressReporter` when `Task.isCancelled` returns `true`. +// Library code +func newFunction() async { + // also implicitly consumes task local progress +} -### Introduce `totalCount` and `completedCount` properties as `UInt64` -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 `FileProgressProperties`, which developers can set and display within `localizedDescription`. +func g() async { + await newFunction() // consumes task local progress + // no more progress to consume here +} -### Store Existing `Progress` in TaskLocal Storage -This would allow a `Progress` object to be stored in Swift `TaskLocal` storage. This allows the implicit model of building a progress tree to be used from Swift Concurrency asynchronous contexts. In this solution, getting the current `Progress` and adding a child `Progress` is done by first reading from TaskLocal storage when called from a Swift Concurrency context. This method was found to be not preferable as we would like to encourage the usage of the explicit model of Progress Reporting, in which we do not depend on an implicit TaskLocal storage and have methods that report progress to explicitly accepts a `Progress` object as a parameter. +// App code +func f() async { + // Did not change, but the reporting behavior has changed + var progressManager = ProgressManager(totalCount: 1) + await g() +} +``` ### 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: @@ -902,27 +843,43 @@ extension Progress { ``` This method would mean that we are altering the usage pattern of pre-existing `Progress` API, which may introduce more confusions to developers in their efforts to move from non-async functions to async functions. -### Allow for Assignment of `ProgressReporter` to Multiple Progress Reporter Trees -The ability to assign a `ProgressReporter` to be part of multiple progress trees means allowing for a `ProgressReporter` to have more than one parent, would enable developers the flexibility to model any type of progress relationships. +### Implement this Progress Reporting API using `AsyncStream` +While using `AsyncStream` would allow developers to report progress with any type of their choice to represent progress, which gives great flexibility to developers in progress reporting, it makes the progress reporting API surface difficult to use for most of the simple cases of progress reporting that merely uses integer fractions. In line with the philosophy of progressive disclosure, we introduce the use of integers as the type to report progress with, and the ability for developers to declare type-safe additional properties that are discoverable at runtime via `@dynamicMemberLookup`. This allows us to cater to both the simple needs of reporting progress with a `totalCount` and `completedCount`, and an additional mechanism for propagating additional metadata throughout the progress graph. -However, allowing the freedom to add a ProgressReporter to more than one tree compromises the safety guarantee we want to provide in this API. The main safety guarantee we provide via this API is that `ProgressReporter` will not be used more than once because it is always instantiated from calling reporter(totalCount:) on a ~Copyable `Subprogress` instance. +### Implement this Progress Reporting API as a Generic Class +In Version 1 of this proposal, we proposed implementing `ProgressManager` using the name `ProgressReporter` as a generic class, which has a type parameter `Properties`, which conforms to the protocol `ProgressProperties`. In this case, the API reads as `ProgressReporter`. This was implemented as such to account for additional properties required in different use cases of progress reporting. For instance, `FileProgressProperties` is a type of `ProgressProperties` that holds references to properties related to file operations such as `totalByteCount` and `totalFileCount`. The `ProgressReporter` class itself will then have a `properties` property, which holds a reference to its `Properties` struct, in order to access additional properties via dot syntax, which would read as `reporter.properties.totalByteCount`. In this implementation, the typealiases introduced are as follows: -### Replace Count-based Relationships between `ProgressReporter` -The progress-reporting functionality = of each `ProgressReporter` depends on the `totalCount` and `completedCount` properties, both of which are integers. This puts the responsibility onto the developers to make sure that all `assignedCount` add up to the `totalCount` for a correct progress reporting at the top level. + ```swift + public typealias BasicProgressReporter = ProgressReporter + public typealias FileProgressReporter = ProgressReporter + public typealias FileProgress = ProgressReporter.Progress + public typealias BasicProgress = ProgressReporter.Progress + ``` + +However, while this provides flexibility for developers to create any custom types of `ProgressReporter`, some issues that arise include the additional properties of a child `ProgressReporter` being inaccessible by its parent `ProgressReporter` if they were not of the same type. For instance, if the child is a `FileProgressReporter` while the parent is a `BasicProgressReporter`, the parent does not have access to the child's `FileProgressProperties` because it only has reference to its own `BasicProgressProperties`. This means that developers would not be able to display additional file-related properties reported by its child in its localized descriptions without an extra step of adding a layer of children to parent different types of children in the progress reporter tree. -While there are considerations to move away from this due to the extra attention required from developers in refactoring code, `fractionCompleted`, which is a `Double` value, has the most precision when computed from integers. +We decided to replace the generic class implementation with `@dynamicMemberLookup` to rely on it to access additional properties that developers may want to use in progress reporting. This allows `ProgressManager` to all be of the same `Type`, and at the same time retains the benefits of being able to report progress with additional properties such as `totalByteCount` and `totalFileCount`. With all progress reporters in a tree being the same type, a top-level `ProgressReporter` can access any additional properties reported by its children `ProgressManager` without much trouble as compared to if `ProgressManager` were to be a generic class. -### Introduce Additional Convenience for Getting `Subprogress` -We considered introducing a convenience for getting `Subprogress` by calling `subprogress()` without specifying `assigningCount` as an argument. In this case, the `Subprogress` returned will automatically be assigned 1 count of its parent's `totalCount` and parent's `totalCount` will automatically increase by 1. +### Implement this Progress Reporting API as an actor +We considered implementing `ProgressManager` as we want to maintain this API as a reference type that is safe to use in concurrent environments. However, if `ProgressManager` were to be implemented, `ProgressManager` will not be able to conform to `Observable` because actor-based keypaths do not exist as of now. Ensuring that `ProgressManager` is `Observable` is important to us, as we want to ensure that `ProgressManager` works well with UI components in UI frameworks. -However, this convenience would introduce more confusion with developers when they try to use `subprogress()` and `subprogress(assigningCount:)` next to each other because `subprogress(assigningCount:)` does not automatically increase the parent's `totalCount`: +### Make `ProgressManager` not @Observable +We considered making `ProgressManager` not @Observable, and make `ProgressReporter` the @Observable adapter instead. This would limit developers to have to do `manager.reporter` before binding it with a UI component. While this simplifies the case for integrating with UI components, it introduces more boilerplate to developers who may only have a `ProgressManager` to begin with. -```swift -// Developer code -let overall = ProgressReporter(totalCount: nil) -await doSomething(overall.assign()) // totalCount: nil -> 1, assignedCount: 0 -> 1 -await doSomething(overall.assign(count: 2)) // totalCount: 1 (doesn't increase), assignedCount: 1 -> 3 -``` +### Not exposing read-only variables in `ProgressReporter` +We initially considered not exposing get-only variables in `ProgressReporter`, which would work in cases where developers are composing `ProgressReporter` into multiple different `ProgressManager` parents. However, this would not work very well for cases where developers only want to observe values on the `ProgressReporter`, such as `fractionCompleted` because they would have to call `reporter.manager` just to get the properties. Thus we decided to introduce read-only properties on `ProgressReporter` as well. + +### Introduce Method to Generate Localized Description +We considered introducing a `localizedDescription(including:)` method, which returns a `LocalizedStringResource` for observers to get custom format descriptions for `ProgressManager`. In contrast, using a `FormatStyle` aligns more closely with Swift's API, and has more flexibility for developers to add custom `FormatStyle` to display localized descriptions for additional properties they may want to declare and use. + +### Introduce Explicit Support for Cancellation, Pausing, and Resuming of this Progress Reporting API +The existing `Progress` provides support for cancelling, pausing and resuming an ongoing operation tracked by an instance of `Progress`, and propagates these actions down to all of its children. We decided to not introduce support for this behavior as there is support in cancelling a `Task` via `Task.cancel()` in Swift structured concurrency. The absence of support for cancellation, pausing and resuming in `ProgressManager` helps to clarify the scope of responsibility of this API, which is to report progress, instead of owning a task and performing actions on it. + +### Handling Cancellation by Checking Task Cancellation or Allowing Incomplete Progress after Task Cancellation +We considered adding a `Task.isCancelled` check in the `complete(count:)` method so that calls to `complete(count:)` from a `Task` that is cancelled becomes a no-op. We have also considered not completing the progress to reflect the fact that no futher calls to `complete(count:)` are made after a `Task` is cancelled. However, in order to make sure that there is always a consistent state for progress independent of state of task, the `ProgressManager` will always finish before being deinitialized. THROW ERROR INSTEAD; catch error use error handling, if want to provide metadata for the cancellation - use custom property + +### Introduce `totalCount` and `completedCount` properties as `UInt64` +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. ## Acknowledgements Thanks to @@ -936,7 +893,7 @@ Thanks to - [Cassie Jones](https://github.com/porglezomp), - [Konrad Malawski](https://github.com/ktoso), - [Philippe Hausler](https://github.com/phausler), -- Julia Vashchenko +- Julia Vashchenko(https://github.pie.apple.com/julia) for valuable feedback on this proposal and its previous versions. Thanks to From db9c1005c97ede6aa19ecdf364b05cc0dc1d258f Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 28 May 2025 15:13:00 -0700 Subject: [PATCH 12/29] fix typo --- Proposals/0023-progress-reporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 0dd3cbc23..e3ae2a84d 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -53,7 +53,7 @@ This proposal aims to introduce a new Progress Reporting API —— `ProgressMan 2. **Self-Documenting Design**: The types introduced in this API clearly separate the composition from observation of progress and allow developers to make it obvious which methods report progress to clients. -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 is 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. +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`. From 44de6ee5fc349c4104a8dfea24898e8cd083edc5 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 2 Jun 2025 13:35:36 -0700 Subject: [PATCH 13/29] update proposal to v5 --- Proposals/0023-progress-reporter.md | 40 +++++++++++++++++------------ 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index e3ae2a84d..4b2d377c4 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -29,6 +29,9 @@ - Expanded Future Directions - Expanded Alternatives Considered - Moving `FormatStyle` to separate future proposal +* **v5** Minor Updates: + - Renamed `manager(totalCount:)` to `start(totalCount)` + - Expanded Alternatives Considered ## Table of Contents @@ -180,7 +183,7 @@ To begin, let's assume there is a library `FoodProcessor` and a library `Juicer` public class FoodProcessor { func process(ingredients: [Ingredient], subprogress: consuming Subprogress? = nil) async { - let manager = subprogress?.manager(totalCount: ingredients.count + 1) + let manager = subprogress?.start(totalCount: ingredients.count + 1) // Do some work in a function await chop(manager?.subprogress(assigningCount: ingredients.count)) @@ -197,7 +200,7 @@ public class FoodProcessor { public class Juicer { public func makeJuice(ingredients: [Ingredient], subprogress: consuming Subprogress? = nil) async { - let manager = subprogress?.manager(totalCount: ingredients.count) + let manager = subprogress?.start(totalCount: ingredients.count) for ingredient in ingredients { await ingredient.blend() @@ -246,7 +249,7 @@ Here's how you use the proposed API to get the most of its benefits: 1. Pass `Subprogress` as a parameter to functions that report progress. -Inside the function, create a child `ProgressManager` instance to report progress in its own world via `manager(totalCount:)`, as follows: +Inside the function, create a child `ProgressManager` instance to report progress in its own world via `start(totalCount:)`, as follows: ```swift func correctlyUsingSubprogress() async { @@ -256,14 +259,14 @@ func correctlyUsingSubprogress() async { func subTask(subprogress: consuming Subprogress? = nil) async { let count = 10 - let manager = subprogress?.manager(totalCount: count) // returns an instance of ProgressManager that can be used to report progress of subtask + let manager = subprogress?.start(totalCount: count) // returns an instance of ProgressManager that can be used to report progress of subtask for _ in 1...count { manager?.complete(count: 1) // reports progress as usual } } ``` -If developers accidentally try to report progress to a passed-in `Subprogress`, the compiler can inform developers, as the following. The fix is quite straightforward: The only one function on `Subprogress` is `manager(totalCount:)` which creates a manager to report progress on, so the developer can easily diagnose it. +If developers accidentally try to report progress to a passed-in `Subprogress`, the compiler can inform developers, as the following. The fix is quite straightforward: The only one function on `Subprogress` is `start(totalCount:)` which creates a manager to report progress on, so the developer can easily diagnose it. ```swift func subTask(subprogress: consuming Subprogress? = nil) async { @@ -281,10 +284,10 @@ func correctlyCreatingOneSubprogressForOneSubtask() { let overall = ProgressManager(totalCount: 2) let subprogressOne = overall.subprogress(assigningCount: 1) // create one Subprogress - let managerOne = subprogressOne.manager(totalCount: 10) // initialize ProgressManager instance with 10 units + let managerOne = subprogressOne.start(totalCount: 10) // initialize ProgressManager instance with 10 units let subprogressTwo = overall.subprogress(assigningCount: 1) //create one Subprogress - let managerTwo = subprogressTwo.manager(totalCount: 8) // initialize ProgressManager instance with 8 units + let managerTwo = subprogressTwo.start(totalCount: 8) // initialize ProgressManager instance with 8 units } ``` @@ -295,10 +298,10 @@ func incorrectlyCreatingOneSubprogressForMultipleSubtasks() { let overall = ProgressManager(totalCount: 2) let subprogressOne = overall.subprogress(assigningCount: 1) // create one Subprogress - let managerOne = subprogressOne.manager(totalCount: 10) // initialize ProgressManager instance with 10 units + let managerOne = subprogressOne.start(totalCount: 10) // initialize ProgressManager instance with 10 units // COMPILER ERROR: 'subprogressOne' consumed more than once - let managerTwo = subprogressOne.manager(totalCount: 8) // initialize ProgressManager instance with 8 units using same Subprogress + let managerTwo = subprogressOne.start(totalCount: 8) // initialize ProgressManager instance with 8 units using same Subprogress } ``` @@ -389,7 +392,7 @@ func doSomethingWithProgress() -> Progress { // Framework code: Function reporting progress with `Subprogress` func doSomethingWithManager(subprogress: consuming Subprogress) async -> Int { - let manager = subprogress.manager(totalCount: 2) + let manager = subprogress.start(totalCount: 2) //do something manager.complete(count: 1) //do something @@ -545,21 +548,21 @@ overall.addChild(subprogressThree, withPendingUnitCount: 1) You call `ProgressManager`'s `subprogress(assigningCount:)` to create a `Subprogress`. It is a `~Copyable` instance that you pass into functions that report progress. -The callee will consume `Subprogress` and get the `ProgressManager` by calling `manager(totalCount:)`. That `ProgressManager` is used for the function's own progress updates. +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 `manager(totalCount:)` on a Subprogress. +/// A child ProgressManager is then returned by calling `start(totalCount:)` on a Subprogress. public struct Subprogress: ~Copyable, Sendable { /// Instantiates a ProgressManager which is a child to the parent ProgressManager from which the Subprogress is created. /// /// - Parameter totalCount: Total count of returned child `ProgressManager` instance. /// - Returns: A `ProgressManager` instance. - public consuming func manager(totalCount: Int?) -> ProgressManager + public consuming func start(totalCount: Int?) -> ProgressManager } ``` @@ -725,7 +728,7 @@ extension Progress { /// Delegates a portion of totalUnitCount to a future child `ProgressManager` instance. /// /// - Parameter count: Number of units delegated to a child instance of `ProgressManager` - /// which may be instantiated by `Subprogress` later when `manager(totalCount:)` is called. + /// which may be instantiated by `Subprogress` later when `start(totalCount:)` is called. /// - Returns: A `Subprogress` instance. public func makeChild(withPendingUnitCount count: Int) -> Subprogress @@ -770,8 +773,10 @@ As the existing `Progress` already exists, we had to come up with a name other t 1. Alternative to `ProgressManager` - `AsyncProgress` - `ProgressReporter` + - `ProgressHandler` + - `OverallProgress` -We ended up with `ProgressManager` because it correctly associates the API as being something that can do more than reporting progress, and it can give out a portion of its `totalCount` to report subtasks. We did not choose `AsyncProgress` because prefixing an API with the term `Async` connotes to adding asynchronicity to the API, as there is a precedent of APIs doing so, such as `AsyncSequence` adding asynchronicity to `Sequence`. `ProgressReporter` also does not appropriately convey the fact that the API can be used to construct a progress tree and that it seems to be something that is read-only, which can appear confusing to others. +We ended up with `ProgressManager` because it correctly associates the API as being something that can do more than reporting progress, and it can give out a portion of its `totalCount` to report subtasks. We did not choose `AsyncProgress` because prefixing an API with the term `Async` connotes to adding asynchronicity to the API, as there is a precedent of APIs doing so, such as `AsyncSequence` adding asynchronicity to `Sequence`. `ProgressReporter` also does not appropriately convey the fact that the API can be used to construct a progress tree and that it seems to be something that is read-only, which can appear confusing to others. While `ProgressHandler` has the advantage of making the API sound a bit more lightweight, `ProgressManager` more accurately conveys the fact that progress reporting can be executed by "completing" a certain count, or "assigned" to a subprogress. "OverallProgress" feels better suited as a property name instead of a type name. 2. Alternative to `Subprogress` - `ProgressReporter.Link` @@ -784,8 +789,11 @@ While the names `Link`, `Child`, `Token` and `Progress` may appeal to the fact t 3. Alternative to `ProgressReporter` - `ProgressOutput` + - `ProgressMonitor` + - `ProgressReporter.Status` + - `ProgressReporter.Observer` -We decided to use the name `ProgressReporter` for the currency type that can either be used to observe progress or added as a child to another `ProgressManager`. The phrase `reporter` is also suggestive of the fact that `ProgressReporter` is a type that contains read-only properties relevant to progress reporting such as `totalCount` and `completedCount`. +We decided to use the name `ProgressReporter` for the currency type that can either be used to observe progress or added as a child to another `ProgressManager`. In comparison to `output` and `monitor`, the phrase `reporter` is more suggestive of the fact that `ProgressReporter` is a type that contains read-only properties relevant to progress reporting such as `totalCount` and `completedCount`. We also did not choose `ProgressReporter.Status` and `ProgressReporter.Observer` because having this as a nested type causes the name to read too long. ### Introduce this Progress Reporting API to Swift standard library In consideration for making `ProgressManager` a lightweight API for server-side developers to use without importing the entire `Foundation` framework, we considered either introducing `ProgressManager` in a standalone module, or including `ProgressManager` in existing Swift standard library modules such as `Observation` or `Concurrency`. However, given the fact that `ProgressManager` has dependencies in `Observation` and `Concurrency` modules, and that the goal is to eventually support progress reporting over distributed actors, `Foundation` framework is the most ideal place to host the `ProgressReporter` as it is the central framework for APIs that provide core functionalities when these functionalities are not provided by Swift standard library and its modules. From 8293405f1e45c8b2bda33eafc4eae20a47623357 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 3 Jun 2025 10:19:44 -0700 Subject: [PATCH 14/29] add code documentation --- Proposals/0023-progress-reporter.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 4b2d377c4..80ab52413 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -575,17 +575,30 @@ public struct Subprogress: ~Copyable, Sendable { /// It is read-only and can be added as a child of another ProgressManager. @Observable public final class ProgressReporter : Sendable { + /// 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. 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 } - public func withProperties(_ closure: @Sendable (ProgressManager.Values) throws(E) -> T) throws(E) -> T + /// Reads properties that convey additional information about progress. + public func withProperties( + _ closure: (sending ProgressManager.Values) throws(E) -> sending T + ) throws(E) -> T } ``` From 4fab79b20d129aa8844e8fc2268770347e1bd7ca Mon Sep 17 00:00:00 2001 From: Charles Hu Date: Tue, 3 Jun 2025 14:58:41 -0700 Subject: [PATCH 15/29] Update status for SF-0023 to 2nd Review --- Proposals/0023-progress-reporter.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 80ab52413..02509c1ec 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -3,8 +3,10 @@ * Proposal: SF-0023 * Author(s): [Chloe Yeo](https://github.com/chloe-yeo) * Review Manager: [Charles Hu](https://github.com/iCharlesHu) -* Status: **Pitch** -* Review: [Pitch](https://forums.swift.org/t/pitch-progress-reporting-in-swift-concurrency/78112/10) +* Status: **2nd Review Jun. 3, 2025 ... Jun. 10, 2025** +* Review: + * [Pitch](https://forums.swift.org/t/pitch-progress-reporting-in-swift-concurrency/78112/10) + * [First Review](https://forums.swift.org/t/review-sf-0023-progress-reporting-in-swift-concurrency/79474) ## Revision history From 082a48df46c73d3754a4d6053e6b0fedf92b2e2d Mon Sep 17 00:00:00 2001 From: Chloe Yeo Date: Tue, 3 Jun 2025 23:57:26 -0700 Subject: [PATCH 16/29] Update 0023-progress-reporter.md --- Proposals/0023-progress-reporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 02509c1ec..e819b07e8 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -916,7 +916,7 @@ Thanks to - [Cassie Jones](https://github.com/porglezomp), - [Konrad Malawski](https://github.com/ktoso), - [Philippe Hausler](https://github.com/phausler), -- Julia Vashchenko(https://github.pie.apple.com/julia) +- Julia Vashchenko for valuable feedback on this proposal and its previous versions. Thanks to From b45439995b4bf0a216ab680055fa590a7d54546f Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 5 Jun 2025 15:08:30 -0700 Subject: [PATCH 17/29] update name + add total and values to ProgressReporter --- Proposals/0023-progress-reporter.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index e819b07e8..05d00dd0e 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -1,4 +1,4 @@ -# `ProgressReporter`: Progress Reporting in Swift Concurrency +# `ProgressManager`: Progress Reporting in Swift Concurrency * Proposal: SF-0023 * Author(s): [Chloe Yeo](https://github.com/chloe-yeo) @@ -601,6 +601,20 @@ public struct Subprogress: ~Copyable, Sendable { public func withProperties( _ closure: (sending ProgressManager.Values) throws(E) -> sending T ) throws(E) -> T + + /// 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?] + + /// 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 } ``` From 122dd93a302cc0a9d55a67ec31c8e49a13d5afbc Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 11 Jun 2025 00:34:46 -0700 Subject: [PATCH 18/29] update proposal with minor updates --- Proposals/0023-progress-reporter.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 05d00dd0e..7d54718ce 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -33,6 +33,8 @@ - Moving `FormatStyle` to separate future proposal * **v5** Minor Updates: - Renamed `manager(totalCount:)` to `start(totalCount)` + - Changed the return type of `values(of:)` to be an array of non-optional values + - Clarify cycle-detection behavior in `assign(count:to:)` at runtime - Expanded Alternatives Considered ## Table of Contents @@ -153,12 +155,12 @@ Another recommended usage pattern of `Progress`, which involves the `ProgressRep ### `ProgressManager` API -We propose introducing a new progress reporting type called `ProgressManager`. `ProgressManager` is used to report progress. +We propose introducing a new progress reporting type called `ProgressManager`. `ProgressManager` is used to manage the composition of progress by either assigning it, or completing it. In order to compose progress into trees, we also introduce two more types: 1. `Subprogress`: A `~Copyable` type, used when a `ProgressManager` wishes to assign a portion of its total progress to an `async` function. -2. `ProgressReporter`: A class used to report progress to interested observers. This includes one or more other `ProgressManager`s, which may incorporate those updates into their own progress. +2. `ProgressReporter`: A class used to report progress of `ProgressManager` to interested observers. This includes one or more other `ProgressManager`s, which may incorporate those updates into their own progress. ```mermaid block-beta @@ -516,6 +518,8 @@ overall.addChild(subprogressThree, withPendingUnitCount: 1) /// Adds a `ProgressReporter` as a child, with its progress representing a portion of `self`'s progress. /// + /// 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`. @@ -534,7 +538,7 @@ overall.addChild(subprogressThree, withPendingUnitCount: 1) /// /// - Parameter property: Type of property. /// - Returns: Array of values for property. - public func values(of property: P.Type) -> [P.Value?] + public func values(of property: P.Type) -> [P.Value] /// Returns the aggregated result of values where type of property is `AdditiveArithmetic`. /// All values are added together. @@ -606,7 +610,7 @@ public struct Subprogress: ~Copyable, Sendable { /// /// - Parameter property: Type of property. /// - Returns: Array of values for property. - public func values(of property: P.Type) -> [P.Value?] + public func values(of property: P.Type) -> [P.Value] /// Returns the aggregated result of values where type of property is `AdditiveArithmetic`. /// All values are added together. From 2275867c1e70abb4d66f31b44f25999e62a7da49 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 11 Jun 2025 00:45:41 -0700 Subject: [PATCH 19/29] expand future directions --- Proposals/0023-progress-reporter.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 7d54718ce..95bf47f39 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -35,6 +35,7 @@ - Renamed `manager(totalCount:)` to `start(totalCount)` - Changed the return type of `values(of:)` to be an array of non-optional values - Clarify cycle-detection behavior in `assign(count:to:)` at runtime + - Expanded Future Directions - Expanded Alternatives Considered ## Table of Contents @@ -798,6 +799,9 @@ To further safeguard developers from making mistakes of over-assigning or under- ### Support for Non-Integer Formats of Progress Updates To handle progress values from other sources that provide progress updates as non-integer formats such as `Double`, we can introduce a way for `ProgressManager` to either be instantiated with non-integer formats, or a peer instance of `ProgressManager` that works with `ProgressManager` to compose a progress graph. +### Support for Decomposition of Progress / Display of Hierarchy of Progress Subtree +If there happens to be greater demand of a functionality to either decompose a `ProgressManager` or `ProgressReporter` into its constituents, or to display the hierarchy of the subtree with a `ProgressManager` or `ProgressReporter` at its root, we can introduce additive changes to this API. + ## Alternatives considered ### Alternative Names From 0f1c9fd5635380cdd24607b4e0d2ae2b9b13a55d Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 23 Jun 2025 16:03:39 -0700 Subject: [PATCH 20/29] fix typo --- Proposals/0023-progress-reporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 95bf47f39..74da7ac91 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -921,7 +921,7 @@ We considered introducing a `localizedDescription(including:)` method, which ret The existing `Progress` provides support for cancelling, pausing and resuming an ongoing operation tracked by an instance of `Progress`, and propagates these actions down to all of its children. We decided to not introduce support for this behavior as there is support in cancelling a `Task` via `Task.cancel()` in Swift structured concurrency. The absence of support for cancellation, pausing and resuming in `ProgressManager` helps to clarify the scope of responsibility of this API, which is to report progress, instead of owning a task and performing actions on it. ### Handling Cancellation by Checking Task Cancellation or Allowing Incomplete Progress after Task Cancellation -We considered adding a `Task.isCancelled` check in the `complete(count:)` method so that calls to `complete(count:)` from a `Task` that is cancelled becomes a no-op. We have also considered not completing the progress to reflect the fact that no futher calls to `complete(count:)` are made after a `Task` is cancelled. However, in order to make sure that there is always a consistent state for progress independent of state of task, the `ProgressManager` will always finish before being deinitialized. THROW ERROR INSTEAD; catch error use error handling, if want to provide metadata for the cancellation - use custom property +We considered adding a `Task.isCancelled` check in the `complete(count:)` method so that calls to `complete(count:)` from a `Task` that is cancelled becomes a no-op. We have also considered not completing the progress to reflect the fact that no futher calls to `complete(count:)` are made after a `Task` is cancelled. However, in order to make sure that there is always a consistent state for progress independent of state of task, the `ProgressManager` will always finish before being deinitialized. ### Introduce `totalCount` and `completedCount` properties as `UInt64` 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. From ac33af6a3189f306aa02d4e848a53deba0e450cd Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 27 Jun 2025 12:15:51 -0700 Subject: [PATCH 21/29] update proposal --- Proposals/0023-progress-reporter.md | 35 ++++++++++++++++++----------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 74da7ac91..2bca5b4b3 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -6,7 +6,9 @@ * Status: **2nd Review Jun. 3, 2025 ... Jun. 10, 2025** * Review: * [Pitch](https://forums.swift.org/t/pitch-progress-reporting-in-swift-concurrency/78112/10) + * [Second Pitch](https://forums.swift.org/t/pitch-2-progressmanager-progress-reporting-in-swift-concurrency/80024) * [First Review](https://forums.swift.org/t/review-sf-0023-progress-reporting-in-swift-concurrency/79474) + * [Second Review](https://forums.swift.org/t/review-2nd-sf-0023-progressreporter-progress-reporting-in-swift-concurrency/80284) ## Revision history @@ -27,14 +29,16 @@ - 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 - 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:)` 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 - - Clarify cycle-detection behavior in `assign(count:to:)` at runtime + - 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 @@ -59,7 +63,7 @@ This proposal aims to introduce a new Progress Reporting API —— `ProgressMan 1. **Swift Concurrency Integration**: This API enables smooth, incremental progress reporting within async/await code patterns. -2. **Self-Documenting Design**: The types introduced in this API clearly separate the composition from observation of progress and allow developers to make it obvious which methods report progress to clients. +2. **Self-Documenting Design**: The types introduced in this API clearly separate the composition of progress from observation of progress and allow developers to make it obvious which methods report progress to clients. 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. @@ -345,7 +349,6 @@ overall.assign(count: 3, to: examCountdown.progressReporter) // Add `ProgressReporter` to another parent `ProgressManager` with different assigned count let deadlineTracker = ProgressManager(totalCount: 2) overall.assign(count: 1, to: examCountdown, progressReporter) - ``` ### Reporting Progress With Type-Safe Custom Properties @@ -447,12 +450,12 @@ 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 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. ```swift /// An object that conveys ongoing progress to the user for a specified task. @available(FoundationPreview 6.2, *) -@Observable public final class ProgressManager : Sendable, Hashable, Equatable, CustomDebugStringConvertible { +@Observable public final class ProgressManager : Sendable, Hashable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { /// The total units of work. public var totalCount: Int? { get } @@ -477,6 +480,9 @@ overall.addChild(subprogressThree, withPendingUnitCount: 1) /// A `ProgressReporter` instance, used for providing read-only observation of progress updates or composing into other `ProgressManager`s. public var reporter: ProgressReporter { get } + /// A description. + public var description: String { get } + /// A debug description. public var debugDescription: String { get } @@ -580,7 +586,7 @@ public struct Subprogress: ~Copyable, Sendable { /// 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 { +@Observable public final class ProgressReporter : Sendable, CustomStringConvertible, CustomDebugStringConvertible { /// The total units of work. public var totalCount: Int? { get } @@ -601,6 +607,12 @@ public struct Subprogress: ~Copyable, Sendable { /// 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( @@ -799,8 +811,8 @@ To further safeguard developers from making mistakes of over-assigning or under- ### Support for Non-Integer Formats of Progress Updates To handle progress values from other sources that provide progress updates as non-integer formats such as `Double`, we can introduce a way for `ProgressManager` to either be instantiated with non-integer formats, or a peer instance of `ProgressManager` that works with `ProgressManager` to compose a progress graph. -### Support for Decomposition of Progress / Display of Hierarchy of Progress Subtree -If there happens to be greater demand of a functionality to either decompose a `ProgressManager` or `ProgressReporter` into its constituents, or to display the hierarchy of the subtree with a `ProgressManager` or `ProgressReporter` at its root, we can introduce additive changes to this API. +### Support for Displaying Children of Progress Subtree + If there are greater demand of a functionality to display the children of a root `ProgressManager` or `ProgressReporter`, we can introduce additive changes to this API. ## Alternatives considered @@ -912,10 +924,7 @@ We considered implementing `ProgressManager` as we want to maintain this API as We considered making `ProgressManager` not @Observable, and make `ProgressReporter` the @Observable adapter instead. This would limit developers to have to do `manager.reporter` before binding it with a UI component. While this simplifies the case for integrating with UI components, it introduces more boilerplate to developers who may only have a `ProgressManager` to begin with. ### Not exposing read-only variables in `ProgressReporter` -We initially considered not exposing get-only variables in `ProgressReporter`, which would work in cases where developers are composing `ProgressReporter` into multiple different `ProgressManager` parents. However, this would not work very well for cases where developers only want to observe values on the `ProgressReporter`, such as `fractionCompleted` because they would have to call `reporter.manager` just to get the properties. Thus we decided to introduce read-only properties on `ProgressReporter` as well. - -### Introduce Method to Generate Localized Description -We considered introducing a `localizedDescription(including:)` method, which returns a `LocalizedStringResource` for observers to get custom format descriptions for `ProgressManager`. In contrast, using a `FormatStyle` aligns more closely with Swift's API, and has more flexibility for developers to add custom `FormatStyle` to display localized descriptions for additional properties they may want to declare and use. +We initially considered not exposing get-only variables in `ProgressReporter`, which would work in cases where developers are composing `ProgressReporter` into multiple different `ProgressManager` parents. However, this would not work very well for cases where developers only want to observe values on the `ProgressReporter`, such as `fractionCompleted` because they would have to call `reporter.manager` just to get the properties. Thus we decided to introduce read-only properties on `ProgressReporter` as well. ### Introduce Explicit Support for Cancellation, Pausing, and Resuming of this Progress Reporting API The existing `Progress` provides support for cancelling, pausing and resuming an ongoing operation tracked by an instance of `Progress`, and propagates these actions down to all of its children. We decided to not introduce support for this behavior as there is support in cancelling a `Task` via `Task.cancel()` in Swift structured concurrency. The absence of support for cancellation, pausing and resuming in `ProgressManager` helps to clarify the scope of responsibility of this API, which is to report progress, instead of owning a task and performing actions on it. From 10dae789279ee8ad796d62a35af3586622d8efcc Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 27 Jun 2025 12:20:02 -0700 Subject: [PATCH 22/29] reformatting --- Proposals/0023-progress-reporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 2bca5b4b3..65a002cf3 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -6,8 +6,8 @@ * Status: **2nd Review Jun. 3, 2025 ... Jun. 10, 2025** * Review: * [Pitch](https://forums.swift.org/t/pitch-progress-reporting-in-swift-concurrency/78112/10) - * [Second Pitch](https://forums.swift.org/t/pitch-2-progressmanager-progress-reporting-in-swift-concurrency/80024) * [First Review](https://forums.swift.org/t/review-sf-0023-progress-reporting-in-swift-concurrency/79474) + * [Second Pitch](https://forums.swift.org/t/pitch-2-progressmanager-progress-reporting-in-swift-concurrency/80024) * [Second Review](https://forums.swift.org/t/review-2nd-sf-0023-progressreporter-progress-reporting-in-swift-concurrency/80284) From 3d88bb1b8f9290af1ac98e52956d2f1dcfb751a3 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 27 Jun 2025 13:34:50 -0700 Subject: [PATCH 23/29] expand alternatives considered + mark accepted --- Proposals/0023-progress-reporter.md | 51 +++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 65a002cf3..f31ce8131 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -3,7 +3,7 @@ * Proposal: SF-0023 * Author(s): [Chloe Yeo](https://github.com/chloe-yeo) * Review Manager: [Charles Hu](https://github.com/iCharlesHu) -* Status: **2nd Review Jun. 3, 2025 ... Jun. 10, 2025** +* Status: **Accepted** * Review: * [Pitch](https://forums.swift.org/t/pitch-progress-reporting-in-swift-concurrency/78112/10) * [First Review](https://forums.swift.org/t/review-sf-0023-progress-reporting-in-swift-concurrency/79474) @@ -886,6 +886,8 @@ 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: @@ -920,8 +922,45 @@ We decided to replace the generic class implementation with `@dynamicMemberLooku ### Implement this Progress Reporting API as an actor We considered implementing `ProgressManager` as we want to maintain this API as a reference type that is safe to use in concurrent environments. However, if `ProgressManager` were to be implemented, `ProgressManager` will not be able to conform to `Observable` because actor-based keypaths do not exist as of now. Ensuring that `ProgressManager` is `Observable` is important to us, as we want to ensure that `ProgressManager` works well with UI components in UI frameworks. -### Make `ProgressManager` not @Observable -We considered making `ProgressManager` not @Observable, and make `ProgressReporter` the @Observable adapter instead. This would limit developers to have to do `manager.reporter` before binding it with a UI component. While this simplifies the case for integrating with UI components, it introduces more boilerplate to developers who may only have a `ProgressManager` to begin with. +### Make `ProgressManager` not `Observable` +We considered making `ProgressManager` not `Observable`, and make `ProgressReporter` the `Observable` adapter instead. This would limit developers to have to do `manager.reporter` before binding it with a UI component. While this simplifies the case for integrating with UI components, it introduces more boilerplate to developers who may only have a `ProgressManager` to begin with. + +### Support for Multi-parent Use Cases +We considered introducing only two types in this API, `ProgressManager` and `Subprogress`, which would enable developers to create a tree of `ProgressManager` to report progress. However, this has two limitations: + - It assumes a single-parent, tree-based structure. + - Developers would have to expose a mutable `ProgressManager` to its observers if they decide to have `ProgressManager` as a property on a class. For example: + ```swift + class DownloadManager { + var progress: ProgressManager { get } // to be observed by developers using DownloadManager class + } + + let observedProgress = DownloadManager().progress + observedProgress.complete(count: 12) // ⚠️: ALLOWED, because `ProgressManager` is mutable!! + ``` +To overcome the two limitations, we decided to introduce an additional type, `ProgressReporter`, which is a read-only representation of a `ProgressManager`, which would contain the calculations of progress within `ProgressManager`. The `ProgressReporter` can also be used to safely add the `ProgressManager` it wraps around as a child to more than one `ProgressManager` to support multi-parent use cases. This is written in code as follows: + +```swift +class DownloadManager { + var progressReporter: ProgressReporter { + get { + progressManager.reporter + } + } // wrapper for `ProgressManager` in `DownloadManager` class + + private let progressManager: ProgressManager // used to compose progress in DownloadManager class +} +``` + +Authors of DownloadManager can expose `ProgressReporter` to developers without allowing developers to mutate `ProgressManager`. Developers can also freely use `ProgressReporter` to construct a multi-parent acyclic graph of progress, as follows: +```swift +let myProgress = ProgressManager(totalCount: 2) + +let downloadManager = DownloadManager() +myProgress.assign(count: 1, to: downloadManager.progress) // add downloadManager.progress as a child + +let observedProgress = downloadManager.progress +observedProgress.complete(count: 12) // ✅: NOT ALLOWED, `ProgressReporter` is read-only!! +``` ### Not exposing read-only variables in `ProgressReporter` We initially considered not exposing get-only variables in `ProgressReporter`, which would work in cases where developers are composing `ProgressReporter` into multiple different `ProgressManager` parents. However, this would not work very well for cases where developers only want to observe values on the `ProgressReporter`, such as `fractionCompleted` because they would have to call `reporter.manager` just to get the properties. Thus we decided to introduce read-only properties on `ProgressReporter` as well. @@ -935,6 +974,12 @@ We considered adding a `Task.isCancelled` check in the `complete(count:)` method ### Introduce `totalCount` and `completedCount` properties as `UInt64` 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 the `Mutex` guarding `ProgressManager` will not be held across both operations, thus creating a race condition, resulting in the `totalCount` that can either reflects 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. + +### 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` set to an `Int`. + ## Acknowledgements Thanks to - [Tony Parker](https://github.com/parkera), From 397b9d5c58a09c04aca9bbecee92ed4aae65b1dc Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 27 Jun 2025 13:59:33 -0700 Subject: [PATCH 24/29] fix formatting --- Proposals/0023-progress-reporter.md | 1 - 1 file changed, 1 deletion(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index f31ce8131..eb7ab512c 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -10,7 +10,6 @@ * [Second Pitch](https://forums.swift.org/t/pitch-2-progressmanager-progress-reporting-in-swift-concurrency/80024) * [Second Review](https://forums.swift.org/t/review-2nd-sf-0023-progressreporter-progress-reporting-in-swift-concurrency/80284) - ## Revision history * **v1** Initial version From 1463145dc1120dbdb6812d36c87baf6bd0766a0c Mon Sep 17 00:00:00 2001 From: Chloe Yeo Date: Fri, 27 Jun 2025 15:33:56 -0700 Subject: [PATCH 25/29] fix typos Co-authored-by: Tina L <49205802+itingliu@users.noreply.github.com> --- Proposals/0023-progress-reporter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index eb7ab512c..40fcd8c0c 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -811,7 +811,7 @@ To further safeguard developers from making mistakes of over-assigning or under- To handle progress values from other sources that provide progress updates as non-integer formats such as `Double`, we can introduce a way for `ProgressManager` to either be instantiated with non-integer formats, or a peer instance of `ProgressManager` that works with `ProgressManager` to compose a progress graph. ### Support for Displaying Children of Progress Subtree - If there are greater demand of a functionality to display the children of a root `ProgressManager` or `ProgressReporter`, we can introduce additive changes to this API. + If there are greater demands of a functionality to display the children of a root `ProgressManager` or `ProgressReporter`, we can introduce additive changes to this API. ## Alternatives considered @@ -977,7 +977,7 @@ We considered using `UInt64` as the type for `totalCount` and `completedCount` t 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 the `Mutex` guarding `ProgressManager` will not be held across both operations, thus creating a race condition, resulting in the `totalCount` that can either reflects 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. ### 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` set to an `Int`. +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`. ## Acknowledgements Thanks to From d6cb84ce6a2053ccf8dc43f32f8ae78a27ba486b Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 27 Jun 2025 15:36:54 -0700 Subject: [PATCH 26/29] fix formatting --- Proposals/0023-progress-reporter.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 40fcd8c0c..b84aa97fb 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -928,14 +928,14 @@ We considered making `ProgressManager` not `Observable`, and make `ProgressRepor We considered introducing only two types in this API, `ProgressManager` and `Subprogress`, which would enable developers to create a tree of `ProgressManager` to report progress. However, this has two limitations: - It assumes a single-parent, tree-based structure. - Developers would have to expose a mutable `ProgressManager` to its observers if they decide to have `ProgressManager` as a property on a class. For example: - ```swift - class DownloadManager { - var progress: ProgressManager { get } // to be observed by developers using DownloadManager class - } - - let observedProgress = DownloadManager().progress - observedProgress.complete(count: 12) // ⚠️: ALLOWED, because `ProgressManager` is mutable!! - ``` +```swift +class DownloadManager { + var progress: ProgressManager { get } // to be observed by developers using DownloadManager class +} + +let observedProgress = DownloadManager().progress +observedProgress.complete(count: 12) // ⚠️: ALLOWED, because `ProgressManager` is mutable!! +``` To overcome the two limitations, we decided to introduce an additional type, `ProgressReporter`, which is a read-only representation of a `ProgressManager`, which would contain the calculations of progress within `ProgressManager`. The `ProgressReporter` can also be used to safely add the `ProgressManager` it wraps around as a child to more than one `ProgressManager` to support multi-parent use cases. This is written in code as follows: ```swift From dbf6eb2893d94c950fa01b2e51ef1658d7c3f0d4 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 27 Jun 2025 15:45:46 -0700 Subject: [PATCH 27/29] fix method description --- Proposals/0023-progress-reporter.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index b84aa97fb..ca36cf106 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -625,11 +625,9 @@ public struct Subprogress: ~Copyable, Sendable { public func values(of property: P.Type) -> [P.Value] /// 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 } ``` From 3e4657ee65d9c605c0ec8dd8da7ff8074baeffe0 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 27 Jun 2025 15:52:00 -0700 Subject: [PATCH 28/29] remove implementation detail --- Proposals/0023-progress-reporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index ca36cf106..57dea9df4 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -972,7 +972,7 @@ 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 the `Mutex` guarding `ProgressManager` will not be held across both operations, thus creating a race condition, resulting in the `totalCount` that can either reflects 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 `withProperties` 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`. From 611c4b2ea7707cc57c781889afe36a7bacc30256 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 27 Jun 2025 16:22:33 -0700 Subject: [PATCH 29/29] add more discussions about additional properties --- Proposals/0023-progress-reporter.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 57dea9df4..05dfbcfa9 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -618,16 +618,22 @@ public struct Subprogress: ~Copyable, Sendable { _ closure: (sending ProgressManager.Values) throws(E) -> sending T ) throws(E) -> T - /// Returns an array of values for specified property in subtree. + /// 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] - /// Returns the aggregated result of values where type of property is `AdditiveArithmetic`. - /// - /// - Parameters: - /// - property: Type of property. + /// 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. + /// + /// - 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 } ``` @@ -640,6 +646,8 @@ We pre-declare some of these additional properties that are commonly desired in 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`. + ```swift @available(FoundationPreview 6.2, *) extension ProgressManager {