I spent some time with NSProgress. It’s a pretty recent API to help Apple platform objects report progress to each other in a loosely-coupled manner, as well as handle cancellation easily.
My goal was to more clearly understand its behavior in the context of a desktop app and multiple progress objects feeding into each other.
I made an example project. Here’s what it does.
The rest of this post is just some musings and ranting that I went through while making this project.
I’m not going to explain what NSProgress is. You should just read Ole Begemann’s great post and the platform docs, in that order, that tell you all you need to know about NSProgress’s background, why it exists, what it tries to do etc.
Ole has some code fragments that I used, but it’s nicer to have something that you can just build and run, to see a complete use case, together with the UI quirks etc. So I made a project.
I was expecting this to be a walk in the park, but in the end, it took me 8 hours of solid coding and cursing. Maybe too much for such a silly little thing. But I spent several hours of this on things that were not really related to NSProgress, but rather with my own limitations as an engineer, which brings me to the first topic…
I honestly wanted to do this example in Swift. I really, really did. That’s how I started. Not very far into it, I hit a roadblock, which was creating NSWindowControllers.
A pattern that I find highly usable in Objective-C is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
The best I can tell, there is no way to do this in Swift that does not look insane and ridiculous. See,
-[NSWindowController initWithWindowNibName:] is a convenience initializer on NSWindowController, and Swift is much more restrictive about its initialization behavior in this case. You can’t call the parent’s convenience initializer from your own designated initializer.
Now, on paper, Swift’s initialization behavior looks great. And I admit a lot of it is due to me being lazy and not having fully internalized it yet. It is indeed much more predictable than the Objective-C honey badger that just (mostly) don’t care about what inits you call. But in this case, after fighting with it for a while, my conclusion was “uhhh… no” and just do my small project in Objective-C.
Okay, the other thing was, I fix up some window frames so that when you run the demo, the windows are reasonably positioned relative to one another, and you see what’s happening in all of them. Objective-C is pretty loose and permissive about types here, whereas in Swift, you get to put up with lovely gems like this.
You’ll notice there’s even a “fix it” thing. If you let it fix it, you’ll get an equally unhelpful message.
I really want to like Swift and wanted to use it in this example. But, as you’ve noticed, the last few paragraphs haven’t said anything about NSProgress, because before getting to that, I kept wasting my time fighting with Swift. I haven’t learned it correctly yet, I know. But in this case, I just wanted to get the example done, so I went back to Objective-C so I could actually do something useful.
Different parts of the NSProgress tree are loosely coupled and there’s no way to know which objects implement it and what don’t. As Ole says…
For instance, did you know that NSData now comes with built-in
NSProgresssupport for the
He makes it sound like it’s a good thing, and it could be, sometimes, I guess. Nevermind that I couldn’t see it working as I expected: trying both with local file and web URL-s, I only got binary progress readings (jumping from 0 to 1 without any intermediate progress).
But, at one point I was seriously confused because I got erratic progress readings. My progress went from 0, to 1, to 0.5, and then gradually from 0.5 to 1. Or even jumping more times between 0 and 1 before finally settling on something like 0.66 or 0.75 and going on from there.
My code looked something like this:
1 2 3
This progress was behaving wrongly. It took me a while to figure out that this is because of the window showing happening after your progress becoming current. Because first-time window showing causes the NIB to be loaded from disk, and how is that done?
+[NSData dataWithContentsOfURL:], of course.
Some method call that I was not expecting to have anything to do with my progress metering at all, was suddenly interfering with it, and giving me unwanted progress readings. See an example that isolates this behavior.
Now, I was able to fix my demo project because it is a small, tightly controlled thing. I can be pretty sure that no other unwanted stuff is running after I fixed the above (first show window, and only then become the current progress object). But this might not be feasible for bigger projects where more things are happening at the same time and you don’t have tight control over all of them. Like, during the time where you are the current progress object, can you guarantee that nobody in your app is going to read anything from disk?
I’m not sure what’s the way out here. Should you be the current progress object for the shortest time possible, and resign it as soon as you can? It’s OK to construct the tree and then resign current, since you no longer need to be current during the actual work and its observation. But this means you might miss some elements of the tree that are instantiated later.
I suppose another solution is what Ole describes, to forgo the whole “current progress object” business and just pass the parent around between objects, and you can then become a child of that particular one and not just a child of whatever happens to be “current” at that point. This defeats the “loose coupling” aspect, though.
UPDATE: … though, on the previous paragraph, I missed one thing in the docs:
@jaanus noticed that the API docs mention that you cannot pass just any NSProgress as parent. Must be nil or currentProgress.— thomvis88 (@thomvis88) January 26, 2015
Well… that certainly makes it more interesting. I don’t know how to react to that or what to suggest for constructing your NSProgress tree in an environment where you may have unknown objects broadcast unwanted progress updates.
NSProgress was crashy for me in this demo project. It doesn’t crash at every run, but if you run the example a few times, you’ll soon enough hit something that looks like this in a NSZombiefied run, when just trying to increment the completed unit count of an instance:
1 2 3
I’ve filed a Radar.
The above has been mostly complaining. Cancellation, on the other hand, is an absolute joy to build with NSProgress approach, and really drives home the point of the “progress object tree”.
- For each NSProgress instance that should run some cleanup work when it is cancelled, you give a cancellation block.
- When you want to cancel a bunch of operations that are tied together in a progress object tree, you call
[progress cancel]on the topmost parent (tree root). This causes all NSProgresses in its tree to be cancelled.
- For long-running code blocks that may be running after the cancellation, you can check
progress.cancelledand bail out of that’s the case.
Very straightforward and you’ll see all of the above covered in my example.
My goal was to understand NSProgress better, and I now understand what to look out for when producing or consuming things built with it. The two things that I worry about most are the crashiness and the unwanted crosstalk from things that you weren’t expecting to report progress.