Hi all,
I have developed a pitch to introduce a new API in Foundation —— ProgressReporter
—— to support progress reporting in Swift Concurrency.
Feel free to leave your feedback, thoughts, comments or questions here!
ProgressReporter
: Progress Reporting in Swift Concurrency
- Proposal: SF-NNNN
- Author(s): Chloe Yeo
- Review Manager: TBD
- Status: Draft
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
.
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:
public typealias BasicProgressReporter = ProgressReporter<BasicProgressProperties>
public typealias FileProgressReporter = ProgressReporter<FileProgressProperties>
public typealias FileProgress = ProgressReporter<FileProgressProperties>.Progress
public typealias BasicProgress = ProgressReporter<BasicProgressProperties>.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.
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:
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)
}
}
}
For the complete proposed solution, detailed design and more in-depth info, check out the full proposal on the PR to the swift-foundation repo.