Earlier this year, I wrote about NSProgress. This year’s releases of El Capitan and iOS 9 bring important updates to how you work with this API. Also, Swift has improved in the meantime. So, the time had come to update my example.
TL;DR: updated example code (requires El Capitan). Video of its behavior below.
Apple’s materials and guidance
Until this summer, there wasn’t much official guidance besides the header available about NSProgress. At WWDC 15, Apple had a nice full session: Session 232: Best practices for progress reporting. Alongside with the material, they provide a nice complete official iOS example project, PhotoProgress, that shows how to work with the API for a photo browser kind of app that uses UICollectionView for its presentation.
The Apple example is great and shows many best practices. There isn’t much in way of OS X, though, and the example may be a bit too deep if this is your first time seeing NSProgress. So I figured it’s still useful to have a simple example on OS X as well, and I updated (well, more like rewrote from scratch) my example project.
One important conceptual update is that we can now use and compose NSProgress in two ways. Implicit composition is what was there before, with the notion of the current progress object for a thread. That continues to work as before. In addition, though, there is a new method of explicit composition that Apple recommends using if you control all the pieces of a system. In some sense, this single new NSProgress API makes all the difference:
/* Directly add a child progress to the receiver, assigning it a portion of the receiver's total unit count. */ @available(OSX 10.11, *) public func addChild(child: NSProgress, withPendingUnitCount inUnitCount: Int64)
Rearchitecting with Swift and storyboards
I said some harsh words about Swift in my previous post on NSProgress. I tried to make the example project in Swift, but just couldn’t do it, as I couldn’t even call some basic API-s to convert between rects. In the meantime, both myself and Swift have evolved and grown up. Swift has gotten to version 2.0, greatly improved its tooling (especially error messages and feedback to engineer) and I’ve done quite a bit of tinkering myself. I wouldn’t yet say I’m a Swift expert, and some of the advanced concepts and examples easily make me put my hands up and say I can’t make heads or tails of them, but I think I can reason about Swift in a much more coherent way than I could at the beginning of the year.
Besides switching the language, I also wanted to try out Mac storyboards. I haven’t really tried storyboards in a major Mac project, but they’re here to stay, so why not try them out. Storyboards definitely steer you towards certain practices on Mac, like thinking much more about view controllers and less about window controllers.
Mostly I’m happy with how the storyboards worked out in this project. The only thing I could not figure out is how to do a nice clean unwind segue. Unwinding works quite differently on iOS and Mac, and this year, there were major updates to unwinding on iOS9. I watched the material, but didn’t quite get my head around it yet. So I ended up putting in a hardcoded solution to dismiss the sheet. Quite clean, but I’d like the presentation and dismissing code to be balanced: if one happens with a segue, the other should match that.
All in all, the Swift language, storyboards and its protocol-driven development drove me towards a much cleaner architecture for the example. The previous example, I’m sorry to say, was a mess, now that I look back at it. Here’s the ways in which the new example is superior:
- The worker task is now a separate UI-less object. This also results in much cleaner ownership of the actual NSProgress object.
- The progress sheet is a separate reusable UI component instead of being redundantly embedded in the XIB-s of each window controller.
- The threading model is much clearer and there are less bugs in this example. I’m actually not aware of any. In the old example, I posted the progress update asynchronously on the main queue from the worker queue, which seems like a strange and crazy thing to do, but I did it back in the day to (mostly but not fully) get rid of bindings-related crashes.
- Pausing and resuming.
Accouting for task weights
The new explicit composition brings along a much clearer way of working with task weights (or child NSProgress weights). Let’s look at this slightly modified piece of code from my example.
// The weights to give to each task in accounting for their progress. let firstTaskWeight = 1 let secondTaskWeight = 9 let totalWeight = firstTaskWeight + secondTaskWeight progress = NSProgress(totalUnitCount: totalWeight) worker1.startTask() worker2.startTask() progress.addChild(worker1.progress, withPendingUnitCount: firstTaskWeight) progress.addChild(worker2.progress, withPendingUnitCount: secondTaskWeight)
I don’t think it was possible to do in the old way of implicit composition, and this should result in much cleaner progress feedback UI-s in many cases.
Here’s what’s going on. We are setting up two tasks here, and we know that the first one takes roughly 10% and the second one 90% of time. Note that this is measured in abstract units in the parent progress unit system. There’s no info about what kind of work the tasks are actually going to do and how they measure it and what units it happens in. All we know is that we allocate 10% of the progress bar horizontal space to the first task, and 90% to the second. This does not mean they need to be happen in sequence: they can fully happen in parallel, and update their own NSProgress objects, and the parent progress is accordingly updated.
Monitoring the updates with KVO and bindings
My old example used Cocoa Bindings to automatically update the horizontal progress indicators from NSProgress objects without any code—or so I thought. There still needed to be code, since NSProgress updates can happen on any queue, but bindings is not happy if it doesn’t get what it needs on the main queue.
Well, bindings are not worth it. Instead, good old KVO is used for all the update monitoring in both my and Apple’s examples. It’s a bit strange to see the KVO code in Swift with its context C pointer and all, but it seems to work fine. So, instead of bindings, I listen to KVO and update the UI “manually”, making sure to post the work into the right (main) queue. There’s actually quite a lot of KVO happening, as you see: the view controllers monitor for progress completion and cancellation, while the progress sheet monitors
fractionCompleted to update the bars. It all seems quite nice in the end.
One final word about KVO straight from the horse’s (Apple’s) mouth: to monitor when an NSProgress object completes its work, don’t observe its fractionCompleted for when it reaches 1.0, since floating point math is still hard for computers and this may be inaccurate. Instead, look for
progress.completedUnitCount >= progress.totalUnitCount, as I and Apple do in the examples.