Streaming is available in most browsers,
and in the WWDC app.
Best Practices for Progress Reporting
NSProgress lets you easily and accurately report the progress of work in your app. Understand the concepts behind progress reporting and how to design code that accurately and efficiently reports its progress. Gain specific insights on how to receive progress updates from framework APIs, fold that into your own progress reporting, and show that progress in your app. Hear from the experts about best practices and how to master the NSProgress APIs.
VINCE SPADER: Hello. My name is Vince Spader. I'm an engineer on the Cocoa frameworks. And today I'm going to be talking to you about Best Practices for Progress Reporting, which in Cocoa means NSProgress.
So I'm going to give you an introduction in NSProgress, then we'll talk about composing NSProgress objects together, and then how to use NSProgress as an interface for cancellation, pausing, and resuming.
Then we'll talk a little bit about hooking NSProgress up to your user interface, and we'll wrap up with some tips and best practices. So let's get started.
NSProgress is an object in Foundation that represents the completion of some work. That work could be downloading a file, installing an app, or something your own application is doing.
The NSProgress object exists to let you easily report progress in your application across various components, both yours and the system's. In fact, several Cocoa APIs are reporting their progress via NSProgress, like NSBundle Resource Request, UIDocument, and NSData. So you can use NSProgress to get information about what those APIs are doing in your application. And NSProgress is localized. You can use it to show information to the user about what's happening. And we have ways to influence what it says, which we'll get to soon.
But first, these are the core properties on NSProgress. We have the totalUnitCount, which is how much work there is to do, and the completedUnitCount, which is how much work has been completed. And that gets updated as the work occurs.
And the fractionCompleted is a double that gets updated from 0 to 1, letting you know how far along the work is toward being completed. So, totalUnitCount and completedUnitCount, the unit these properties are referring to is up to you. It is whatever unit it makes sense to track in terms of progress. Perhaps bytes, number of files, photos, or even abstract units like percentage points or fractions of work. Each individual NSProgress object has its own idea of what unit it is reporting in. And if you don't know how much work there is total, so you don't know your totalUnitCount, you can make your progress indeterminant. And you do that by either setting the completedUnitCount or the totalUnitCount to a negative value. Next, let's talk about localization.
NSProgress has two properties, localized Description and localized Additional Description, that you can display to the user.
You can set them yourself but NSProgress will also come up with something for you by default.
So, here is an example of the default localized Description and localized Additional Description. We have an NSProgress with a totalUnitCount of 5 million and some, and a completedUnitCount of 419 thousand and change. The default localized Description is 7 percent completed, and the localized Additional Description formats those numbers nicely.
So this is the default. And if you want something different, you could set the localized Description and localized Additional Description yourself but then you would need to actually localize it in the languages your app supports. And instead of doing that, we have a couple of knobs that you can use to alter those defaults. So the first is through the kind property. Currently the only option is for files, NSProgressKindFile.
Using this means your units are bytes, so when NSProgress knows your units are bytes, it can format them as such, so you see now it can say 419 kilobytes of 5.3 megabytes.
The other knob for changing the default localized Descriptions is through certain keys in the user info dictionary. So NSProgress has a user info dictionary, there is a method, Set User Info Object For Key, that lets you set a key and value in the userInfo.
And one key that's useful for virtually any kind of NSProgress is NSProgress Estimated Time Remaining Key. The value there is an NSNumber of how many seconds are remaining until the work is completed.
And you can see if we set that to, say, 97 seconds, our localized Additional Description now includes that information formatted as 1 minute, 37 seconds remaining.
And there are additional userInfo keys that are only used if your kind is set to file. First let's look at NSProgress File Operation Kind Key. This tells NSProgress the type of operation that is being performed on the file. The values are either downloading, decompressing, receiving, or copying, so if you set the File Operation Kind to NSProgress File Operation Kind Downloading, that updates the localized Description to say it is downloading files. Another key available when your Kind is File is the NSProgress File URL Key. And that is an NSURL of the file being worked on. So when you set that, the localized Description will include the name of the file, in this example, photos.zip, which is from the given URL.
There are also options for if you're operating on a set of files. NSProgress File Total Count Key and Completed Count Key. So, here's an example where we're setting the File Completed Count to 7, and the File Total Count to 9, and the localized Description might use that information, so now it says, downloading 9 files.
Note that the kind is still File and the units are still bytes, it is just the total bytes of the files being worked on.
And finally, we have the NSProgress Throughput Key. And that's the bytes-per-second that the file operations are being performed at. So, say, it is downloading at a blazing fast 50,000 bytes-per-second, if we set the throughput on the userInfo, the NSProgress can include that information in the localized Description, so it says, 50 kilobytes-per-second.
So, all these options can really provide, help you provide more information to the user about what's happening., without you having to localize it yourself.
Now, before we go any further, we need to talk about responsibilities.
There are two sides of NSProgress, there is the creation side of it, and the client side of it.
First the creation side.
So, when you create an NSProgress, you are responsible for setting its properties and updating the completedUnitCount as the work finishes. So you're creating it, you're setting the totalUnitCount, the Kind, setting keys in the userInfo, and updating the completedUnitCount as the work finishes.
On the other hand, if you receive an NSProgress from someone else, you're the client. You can get and observe the various properties, the totalUnitCount and completedUnitCount, the fractionCompleted, or those localizedDescriptions, but do not go setting those properties because that's just going to cause confusion with the creator of the NSProgress.
So when you create an NSProgress, you need a way to give it to clients, and when you're a client you need a way to get it. And one way of doing this is through the NSProgress reporting protocol, which we added in OS X 10.11 and iOS 9. It is pretty simple, and it defines one property, progress.
In Cocoa UIDocument and NSBundleResourceRequest, both implement this. And it makes it really obvious that a class supports progress reporting. All right. Now, let's do a demo and take a look at some code.
So, here we have an app. It is pretty basic.
It has a photo, and you hit this import button and it downloads that photo, and when the download finishes, it shows it to the user.
Now, that's a pretty awful user experience, the user has basically no idea what's happening. And we can make it better by reporting progress to the user for our download. So, if we go to our project, we have our download class that is used to download the photo, and it has an initializer that takes a URL for it to download, and it has a completion handler that gets called with the downloaded data or an error if an error occurs. Next there is a start method which gets -- which kicks the whole download off, and we have a handful of private methods for various download-related functionality.
And we also have these convenience methods that will get called as the download progresses. So we have Will Begin Download, which gets called when the download begins and it gets the total length of the download.
Did Download Data, which gets called periodically as the download completes.
Did Finish Download gets called when the download is completed, and Did Fail To Download is called if there was an error during the download.
So what we're going to do is we want to report progress about this download operation, and we can do that by adopting the NSProgressReporting protocol.
So let's go ahead and go up to our class declaration, and we can add NSProgressReporting to our list of classes.
And in order to conform to NSProgressReporting, we need a ProgressProperty. So let's add that, it's in NSProgress.
And now in our initializer we need to create our NSProgressObject.
And since we don't know how much there is to download yet, we're going to make our progress indeterminant. And one way of making your progress indeterminant is setting the totalUnitCount to a negative value, so we're going to set our totalUnitCount to negative 1.
And since we know we're downloading a file here, we can also give the NSProgress a little bit more information about what's happening. So we can set the Kind to NSProgress Kind File, and we can set in the userInfo, the NSProgress File Operation Kind Key to Downloading. So now we have created our NSProgress and now we just need to update it as the download completes. So, back to our Will Begin Download method, that gets the total amount to download. So we want to set our totalUnitCount to that amount. And at that point the progress will no longer be indeterminant. And in our Did Download Data callback, which is called periodically, we can set our completedUnitCount to the number of bytes downloaded so far.
And finally, in our Did Download Data callback, we can set our completedUnitCount to the total number of bytes in the download, and that way the progress will be finished.
Now if we, now the user interface is already using NSProgressReporting and looking for the download to implement that, and will show the progress, so when we build and run our app, and we press the import button, we have a progress being reported for the download to the user, and it is a much less painful experience.
All right. So, back to slides.
VINCE SPADER: All right. So, that's the basics of NSProgress. Now let's get into what really makes NSProgress powerful, which is the ability to compose progress objects into other progress objects.
Now, just because I press a download button doesn't mean that there is really only one thing happening. There might be the download, a verify, and a decompress operation all running in reporting progress. But the user only sees one progress bar.
So, let's say that these boxes represent individual NSProgress objects. They each report their own progress in their own units without having to worry about the others. But we want them to be combined into a single NSProgress that we hook up to the user interface. So we're going to create an NSProgress object and call it the overall progress.
And we can compose these other progresses into our overall progress, the overall progress becomes a parent of these other three progress objects, the download, verify, and decompress progresses are its children.
And as those children complete, the overall progress will be updated. So for composing NSProgress objects, we have this idea of a pendingUnitCount. The pendingUnitCount is a portion of the parent's totalUnitCount that gets assigned to a child progress object.
The pendingUnitCount is in terms of the parent's units, the child has its own units, and we say that the parent's pendingUnitCount is assigned to the child. So what happens is, when a child progress finishes, the parent's completedUnitCount is incremented by the pendingUnitCount for that child. So when you have a child, you don't update the completedUnitCount manually, that might conflict with an update that happens when your child finishes. When your at parent progress, you really want to assign your entire totalUnitCount to children. So let's go over a diagram of composition. Let's say we're importing some photos. So we have the overall import NSProgress object.
There are two photos total, so the totalUnitCount for our import progress is 2.
And it is going to assign its entire totalUnitCount to its children, which are the progresses for the individual photos below. So each photo is assigned a pendingUnitCount of 1 photo from the overall import progress.
Now, the individual photo progresses are similar but their unit is different, they have a totalUnitCount of two steps.
And each step is taking up one pendingUnitCount, one for the download and one for the filter.
And finally we have the download and filter progresses. They have their own units. They have no children. And they update their completedUnitCount manually.
All right. So I brought in the completedUnitCounts here. That's the zero of. And the fraction completed is the percentage at the bottom. Since we haven't done anything yet, it starts at zero. And let's see what happens as the completedUnitCounts at the bottom are updated.
So you can see these -- as these children complete, the fraction completed is updating in the parents. Progress kind of flows up to the parent.
And note that the completedUnitCount for the overall progress is still zero. The completedUnitCount is only incremented once the child is finished and photo 1 still isn't finished. And once a child does finish, the completedUnitCount is incremented by the pendingUnitCount for the child. You can see that the import progress now has one photo complete since photo one is now 100% finished.
The fraction completed, on the other hand, is multiplying up based on the pendingUnitCount and the fraction completed of the child. It doesn't wait until the child is finished to update.
And when everything is at 100%, the import progress is finished, and that's an example of what happens when you're composing NSProgress objects.
Now let's zoom in here. Here we have a progress for an individual photo's import, so this is just one photo.
There are the two steps, download and filter. So we have a totalUnitCount of 2. And we say the download is going to take one of those units and the filter is going to take one of those units.
So each step takes up half of the photo's overall progress.
But what if these operations aren't equal? What if we know that the filter is pretty quick relative to the time to download, so it looks something more like this? Well, units can be arbitrary. You can say that there are actually ten steps and the download is assigned nine of those, and the filter is assigned one.
And now as the download completes, the download step takes up 90% of the import progress and the filter takes the remaining 10%.
So you can modify the units of the progress in order to weight the work being assigned to children. All right.
Let's see how to actually do this in code. There are two ways of composing NSProgress objects and the first way I'm going to talk about is implicit composition.
So you create a parent progress object. This will be the photoProgress from before.
So there are two steps. And we have -- so we have a totalUnitCount of 2.
And what we want to do, is add a download progress as a child.
So we call become Current With Pending Unit Count on the parent progress, the photoProgress.
And what this does is it sets a thread local current progress so the photoProgress is the current progress, and that pendingUnitCount of 1 is set aside and basically reserved for progress to come along and get added to the current progress.
And next we call our download function, startDownload, and that creates a progress with the NSProgress with totalUnitCount convenience constructor, and with totalUnit convenience constructor, we'll add the created progress to the current progress.
So the download is added as a child of our photoProgress.
Next we need to clean this up. So we call resigned current and the photoProgress is no longer the current progress. And that's it. Now we've used implicit composition to add a child to a parent. A few things to remember when using implicit composition, when supporting implicit composition, you want to create the NSProgress immediately using the with totalUnitCount convenience constructor. That's because the first progress object added to the current progress takes up that entire pendingUnitCount. And if you create it first thing, you don't have to worry about getters or other calls unintentionally taking up the parent's pendingUnitCount.
Also, be sure to document it. Implicit composition is implicit after all, clients won't know you support implicit composition unless you say so. Also, if no child is added by the time you call resigned current, that pendingUnitCount will be immediately added to the parent's completedUnitCount.
So your completedUnitCount will be updated.
So, the second way to compose NSProgress objects is new in OS X 10.11 and iOS 9, and that's called explicit composition.
So, you receive a progress that you want to add as a child from somewhere. Perhaps something that conforms to the NSProgressReporting, we'll say a filter.
And you have your parent progress that you want to add it to.
Let's say the photoProgress from before.
Then you simply call addChild with pendingUnitCount on the progress you want to add it to, and path in your child progress, the filterProgress, here, and give it the pendingUnitCount you want to add it with, since it is one step, we want to add it with a pendingCount of 1.
And that's it. Now your progress is a child of the parent progress. So here are some guidelines for when to use explicit or implicit composition.
If you have a method that can't return in NSProgress like you're crossing an API boundary you can't change, use implicit composition and document that it supports implicit composition.
Or, because explicit composition is new in OS X 10.11 and iOS 9, you'll have to use implicit composition on older releases. Otherwise, you will generally want to use explicit composition; it is a lot simpler. All right.
Now let's go through a demo of composition. So here we have our photo download class that we added our progress reporting to last time. And if we run our app, it has been changed a bit. Now it has a CollectionView of photos instead of a single photo. So, and when we press import, instead of just downloading those images, it is also running them through a filter. So we don't have progress information for that overall thing, so the experience is pretty bad.
And again, we can make it better by having our operations report progress.
So let's do that.
So we have our download class that supports NSProgressReporting, and we have our filter class, which has a class method that takes an image and returns a filtered image.
And we have this import class, which has both the download and runs the filter when the download completes. So it combines the progress for -- we want it to combine the progress for the download and the filter operation.
So let's look at our photo import. It has an initializer, which takes a URL and it creates the download with that URL.
It also has a completion handler that gets called with the filtered and downloaded image, or an error if an error occurs.
And it also has a start method for starting the import. And the start method sets the completion handler on the download, and then creates a UIImage from the downloaded data, and then passes that image into our filter, getting out our filtered image, and then calls its own completion handler with the filtered image. And once the completion handler is set, it starts the download. So what we want to do is have our photo import report a combined progress comprising of both the download and the filter's progress. And we are going to do that again by having our photo import class conform to NSProgressReporting.
So let's go up to our class declaration and add NSProgressReporting.
Now we need to have a progress property, so let's add that.
ANd we need to create our NSProgress object.
And this time for our units we're going to use something a little abstract. We have run the app a few times and we've found that if we have the download take up about 90% of the import's progress, that feels pretty good. So we're going to have a totalUnitCount of 10, and we're going to say the download takes up 9 of that totalUnitCount and the filter is going to take up the remaining 1. So now in our start method, since the download already conforms to NSProgressReporting, we can just get the progress object from it and add it to our progress, and we can do that with the explicit add child method. So we're calling progress add child download progress, and our pendingUnitCount is 9, so it takes up 90% of our progress. And now we want to also add the filter's progress to our progress.
But the filter is a class method that takes an image and returns an image. There is no obvious way to get a progress out of it.
But, if we go to our photo filter class, we can see a comment here that says it supports implicit progress composition. So we can use implicit progress composition to add it as a child.
So let's go back to our import start method.
And in the downloads completion handler, which might be called on a background thread, we're going to become current with the pendingUnitCount of 1.
And note, like I said, the completion handler for the download might be called in a background thread, but that's okay because we're calling the filter immediately after on the same thread.
So, after we become current, our filter will run and it will add itself as a child to the current progress, and once it returns, we need to no longer be the current progress, so we're going to call resign current. And that's it. Now we have added both the download and the filter progress to our import progress. And now if we run our app, and we press import, you can see the imports are reporting progress for each photo.
And that's good, the user has more information on what's going on, but it is not really that great. We only want one progress to show to the user. So let's do that. So we're going to zoom out a little bit and we're going to go to our root View Controller, photos View Controller, and this has an overall progress property. It is a client of this NSProgress, it just gets it, and it is going to hook it up to the UI and show it. It is not going to create it itself.
And it also has a reference to an Album, which is the collection of photos that we're downloading. And it has IBActions for the toolbar buttons, in this case the start import button and that IBAction just calls import photos on our album.
So if we look at our album it has an array of photos, and it creates the photos from URLs in the main bundle, and in its import photos method it goes through each photo and calls start import on them. Now if we look at our photo, our photo has an image URL that it gets in its initializer, and it also has a UIImage property that starts out as a place holder.
It also has its start import method that is getting called by the album. It creates that photo import class that we added NSProgressReporting to, and then sets a completion handler for that photo import class, to set the image as the image that's been imported. And it is then, after the completion handler is set, it starts the import and stashes it away in case it needs it. So what we want to do is we want to get the photo imports progress and collect them into a progress for all of the imports that are happening.
So let's go back up to our root View Controller. And what we can do is we can say that our import progress method returns that NSProgress. so we're going to set our overall progress property to that returned NSProgress.
And import photos doesn't return in NSProgress yet, so we need to go do that. So let's go to our album and its import photos method.
And it is currently returning void. And we need to make it return in NSProgress.
Then we need to create the progress object that we're going to return.
And since this progress object is going to have a child for every photo in our album, we want our totalUnitCount to be the number of photos in the album. And also let's go ahead and return it.
And what we're going to do is we're going to have our photo start import method also return a progress, so we're just going to assign that to a local variable import progress. And then we're going to add that as a child to our -- to the album's progress with the pendingUnitCount of 1, since this is the import progress for one photo. And now, in our photos start import method, it is currently returning void. We want in to return in NSProgress, so let's do that.
And since our photo import already conforms to NSProgressReporting, we can just return the progress property from it.
And that's it. We have quite a composition happening now. We have that overall progress being assigned to a progress made up -- that has children for each import that's happening, and each of those imports has both a download and a filter child.
So let's run the app. And now we have an overall progress at the bottom that gets updated as all those children complete.
Removing all of the smaller progress bars is left as an exercise.
All right. VINCE SPADER: Okay. Back to slides. So now I would like to talk a little bit about cancellation, pausing, and resuming.
NSProgress objects can be a conduit for cancellation. The creator of the NSProgress sets cancelable and the cancellationHandler.
If your operation is doing some work synchronously and and the cancellationHandler doesn't really work, you can pull the canceled flag on the NSProgress object as well. A client can call cancel and the NSProgress will set cancel to true and invoke the cancellationHandler. So cancellation flows down to children. So if you have child progresses with cancellation handlers, those will be invoked too.
And it is permanent. Once a progress is canceled, there is no uncanceling.
Pausing is pretty similar to cancellation. The creator of the NSProgress sets pausable along with a pausing handler and resuming handler. That resuming handler is actually new in OS X 10.11 and iOS 9.
And you can also pull that paused flag to determine if the progress is currently paused. Clients can call pause and will set pause and call the pausing handler, or they can call resume and will unset pause and invoke the resuming handler. Pausing and resuming also flows down to child progresses just like cancellation.
And let's go ahead and do a demo of that. So if your objects, if your operations already support cancellation, pausing, and resuming, it is actually really easy to expose that through NSProgress.
So we're back in our photos View Controller. And this is our root View Controller.
And this time the app has a few more buttons. So if you press import, there is also a cancel and pause button, but they don't really do anything right now. And we need to hook them up.
So let's do that.
So we have our IBActions defined for those buttons, we have cancel import, pause import, and resume import. And what we're going to do is we're going to call cancel, pause, and resume on the overall progress inside of those actions.
And now once -- and now that will call any cancellation, pausing, or resuming handlers on any child progresses. And we don't have any yet, but our download does support cancellation, pausing, and resuming. So let's go to our photo download.
And if we go down to our Will Begin Download callback, we can add cancellation, pausing, resuming support to this NSProgress. So first we set cancelable to true, and we set our cancellationHandler, and here our cancellationHandler is calling Failed Download With Error with an NSUser Canceled Error.
And we also are pausable and resumable, so we're going to set pausable to true, and in our pausing handler we're going to call this Suspend Download method.
And in our resuming handler, we're calling Resume Download.
Now, note that these are all private, the Failed Download With Error, Suspend Download, and Resume Download methods, so we're only exposing this functionality for canceling, pausing, or resuming through NSProgress, so it can be a pretty powerful point of interaction.
Now when we run the app, and we compress, start our import and pause, and the progress, the download pauses itself, and we press resume, and it resumes, and we can hit cancel, and it gets canceled. And what's happening is that overall progress is sending -- invoking any cancellationHandlers for any children it might have.
And that's it.
Back to slides.
VINCE SPADER: All right.
So let's talk a little bit about the user interface. We have gone through all of this trouble of creating these NSProgress objects, but their ultimate purpose is to give the user an idea of what's happening, and that means the user interface.
So all of the properties on NSProgress are key value observable, clients can add the KVO observers to update their UI. For example, a client can update their UI progress views progress with the NSProgress as fractionCompleted property, or update a label with a localizedDescription.
Also be aware, that these KVO callbacks might not necessarily be called on the main thread. So if you're updating UIControls, you want to move that work to the main queue. So here is an example of what that adding an observer might look like.
You call addObserver for key path on the NSProgress for the fractionCompleted property.
Then in our override of observe Value For Key Path we enqueue some work on the main queue, and on the main queue we get the fractionCompleted from the NSProgress, and update our UIProgressView. And that's basically it. You can use a similar pattern for updating labels or buttons in your UI. All right, finally, let's go over some best practice for NSProgress. Since this talk is called best practices, I better squeeze some in at the last minute. So, first is completion. Don't use fractionCompleted to determine completion. It's a floating point value determined -- derived from a calculation. So comparing it to 1.0 won't always be right. Use completedUnitCount and totalUnitCount instead, unless your progress is indeterminant or the totalUnitCount is 0.
It is important, by the way, that progress is finished. The parents completedUnitCount only gets updated when the child finishes. And also NSProgress can optimize a way of completed children so memory can be saved as work finishes.
Next, NSProgress objects cannot be reused. Once they're done, they're done. Once they're canceled, they're canceled. If you need to reuse an NSProgress, instead make a new instance and provide a mechanism so the client of your progress knows that the object has been replaced, like a notification.
Don't update the completedUnitCount in a tight loop. So, for example, don't update it every byte of a download. If you have a parent, we might be calling up to update the fractionCompleted, which might take longer than you're expecting since your composition can be arbitrarily large and very deep.
But when you do so, don't forget to finish the progress. So be sure to update the completedUnitCount to the totalUnitCount. Otherwise you're going to be left with nearly finished progresses. And that's no fun. So that's it. We talked a lot about how to use NSProgress effectively. If there is anything to remember, it is these points. Each progress has its own unit. You can compose them either implicitly or explicitly. And when you do, the pendingUnitCount is in the parent's units.
Also you either create the NSProgress or you're a client.
For localization, you can use the kind and userInfo properties to help us give you a better localizedDescription.
NSProgress can be a really nice interface for cancellation, pausing, and resuming. And all its properties are KVO observable, so you can use it, use that to update your UI. That's it. For more information, see the documentation; also check out the header for NSProgress, it is extremely well commented. We have some new sample code, photoProgress, which is based off the demos I showed today. If you need any help, try the developer forums or contact developer technical support, and for general inquiries, email Paul Marcos. And that's all. Thanks.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.