I’ve been working on a replacement for Progress (NSProgress) for use in my own code for a while now. It hasn’t been extensively battle-tested yet, but for the time being it seems to work. The full code for it is at https://github.com/CharlesJS/CSProgress (BSD licensed, with the exception that Apple/the Swift team is granted permission to treat it as public-domain code if they so choose).
A rather detailed description of this class and its motivations are in the read-me for the linked repository, and copying the whole thing would probably run afoul of the mailing list’s size limit. However, these are the highlights:
- Much better performance. In my testing, updating the completedUnitProperty, with something observing it, takes 1.04 seconds on my 2013 Retinabook, compared with NSProgress’s 52.91 seconds (58.53 seconds if an autorelease pool is used). This frees back-end code from the responsibility of being adulterated with what is effectively UI code in order to determine when the progress object should be updated.
- Much better RAM usage. NSProgress generates a large number of autoreleased objects, which can cause considerable memory bloat if they are not wrapped in an autorelease pool, which further hinders performance. By using Swift-native types whenever available, this problem is avoided.
- Better thread safety, by using atomic operations to update the completedUnitCount property. NSProgress requires taking the lock twice, once to read the old value, and once to write the new value. CSProgress reduces this by allowing the delta to be sent, meanwhile solving the race condition that can occur if something else updates the property in between the two operations.
- Includes a “granularity” property, customizable by the user, which determines how often fractionCompleted notifications will be sent. The default is 0.01, which means that fractionCompleted notifications will be sent whenever fractionCompleted becomes at least 0.01 greater than its value at the time the last notification was sent. For a steady increase from 0 to totalUnitCount, the notification will be sent 100 times. This prevents unnecessary UI updates when the progress is incremented by a negligible amount.
- Much better and “Swiftier” interface:
- All observations are done via closures, rather than KVO.
- All observations are sent via an OperationQueue which is specifiable by the user, and which defaults to the main queue. This means that
UI elements can be directly set from within the notification closures.
- Unlike the closure-based APIs on NSProgress such as cancellationHandler, CSProgress allows multiple closures to be specified for each observation type, in case a progress object has more than one client that wants to observe it.
- Uses generics for all functions taking unit counts, so that any integer can be passed to them instead of having to cast everything to Int64.
- Adds a wrapper struct encapsulating both a parent progress and a pending unit count, so that explicit tree composition can be used without
requiring functions to know the pending unit counts of their parents, thus preserving the loose coupling inherent to the old-fashioned implicit style.
- Bridging to the standard Objective-C Progress/NSProgress. This code was added the most recently, and is the most hackish by far, so it will need a lot of testing, but for the time being it seems to pass my unit tests. The bridging is designed to allow CSProgress to insert itself into any part of an NSProgress tree, being able to handle NSProgress objects as parents or as children. Updates to parent NSProgresses are also done on a user-specified OperationQueue, which defaults to the main queue. This means that if you prefer NSProgress’s KVO-based API, you can actually bind your UI elements directly to the NSProgress, and since all updates to the NSProgress will happen on the main queue, you won’t need any special property observers to forward notifications to the main thread.
I am wondering what the community thinks of this. I’ve tried to keep the interface as close to Progress/NSProgress’s as possible, to allow this to be a drop-in replacement. There are a few features of NSProgress that are still unimplemented; however, I could add them if the response is positive. With the interface mirroring Progress’s, this means that, with some tweaking and extensive battle testing, CSProgress could potentially serve as a replacement for Progress, with the original Progress being renamed back to NSProgress. The result would break binary compatibility, but could be source-compatible, and I think it has the potential to improve the experience on Swift.
What do you think?