3
May
2008
 

Cocoa Tutorial: File Copy With Progress Indicator

by Matt Long

I often find and therefore come to expect common problems in Cocoa to be easily solvable, however, there are times, like this, where I am a bit disappointed that the problem can be so difficult. All I need to do is copy a file. This is a simple task right? All you need is NSFileManager and a call to – (BOOL)copyPath:(NSString *)source toPath:(NSString *)destination handler:(id)handler and you’re good to go. Not so fast, city slicker. This one is going to take a bit more effort cause you want one of them fancy new-fangled progress indicators to show how much of the file has been copied. Well, if you want Cocoa to help you, you’re out of luck cause NSFileManager won’t do that for you, but with a little bit of effort you can get it to work with some… gulp… C APIs.

You can download the demo project for this post here: Demo Project Copy File

It Couldn’t Be Simpler! Well, Maybe A Little Bit

The only example of doing an asynchronous file copy while implementing a callback from Apple is found here in the FSFileOperation example’s main.c file which is some 545 lines of code. I don’t know about you, but I really need small examples to understand what is going on. In fact, that’s one of the highly vaunted goals of our writing here at Cocoa Is My Girlfriend. If it’s not something you can grok in small digestable pieces, then it’s not terribly useful. At least that’s my limitation in learning and my philosophy in writing.

Anyhow, once I started down the path to find the solution, I simply looked at the call to FSCopyObjectAsync which has this signature.

OSStatus FSCopyObjectAsync (
   FSFileOperationRef fileOp,
   const FSRef *source,
   const FSRef *destDir,
   CFStringRef destName,
   OptionBits flags,
   FSFileOperationStatusProcPtr callback,
   CFTimeInterval statusChangeInterval,
   FSFileOperationClientContext *clientContext
);

I then just tried to figure out how to get each parameter that call needs and went from there. I got it about 95% of the way there and then had to ask the Cocoa Dev list why my callback wasn’t working. It was copying the file without a problem, but it wasn’t calling my callback at the specified interval in the CFTimeInterval statusChangeInterval parameter. Apparently, I needed to schedule my callback with the run loop using this call before it would work right:

OSStatus status = FSFileOperationScheduleWithRunLoop(fileOp, runLoop, kCFRunLoopDefaultMode);

The Gory Details

I will spare you those. Actually, it turns out that doing an asynchronous file copy while updating a progress indicator is not that difficult. It’s just isn’t terribly well documented for Cocoa–at least that I could find. I hope this post will help to remedy that. Take a look at the source code in the demo project and see what you think.

One detail I will provide, though, is that you will need to make your progress indicator available to the callback which is declared outside of the AppDelegate. To do so, I simply create a static pointer to an NSProgressIndicator object and then set it equal to my local NSProgressIndicator in the awakeFromNib of my AppDelegate. This allows me to go ahead and hook up my progress indicator in Interface Builder still, while making the reference to the indicator available to my callback. Here is the code I’m referring to:

static NSProgressIndicator *progressIndicator;

@interface AppDelegate : NSObject {
    IBOutlet NSProgressIndicator *localProgressIndicator;
snip...

And then in my -awakeFromNib:

- (void)awakeFromNib;
{
    progressIndicator = localProgressIndicator;
}

My callback is then able to access and update it like this:

CGFloat floatBytesCompleted;
CFNumberGetValue (bytesCompleted, kCFNumberCGFloatType, &floatBytesCompleted);

[progressIndicator setDoubleValue:(double)floatBytesCompleted];
[progressIndicator displayIfNeeded];

Note: Keep in mind that when you run the demo application, you should copy a file that will take longer than one second to copy if you want to see progress. The minimum callback interval in the FSCopyObjectAsync call is 1 second so anything smaller than several hundred MB will probably not be big enough to see the progress indicator advance progressively. This is also why you see a delay when the file copy first starts.

Conclusion

Copying files seems like such a common task so I’m pretty surprised how little documentation there is out there to demonstrate how to do it with a progress indicator in Cocoa. So much so, that I’m going to concede right now that I’ve probably missed something much simpler. If you can share that with me, I would love to know. Give me your feedback in the comments section or shoot me an email at matt at cimgf dot com. Until we meet again.

Comments

hennk says:

Instead of accessing the progress indicator via a static pointer, you can just use the void *info field of the FSFileOperationClientContext struct, and passing either the AppDelegate or the progress indicator itself.

You could also make the AppDelegate a singleton, and add an accessor method for the progress indicator. E.g. [[AppDelegate sharedInstance] progressIndicator]

jediknil says:

Second what hennk said. What if, for some crazy reason, you wanted to have TWO progress indicators? *gasp*

Seriously, though, globals are generally bad, but what seems really awkward here is the limitation of one progress indicator, period. Especially since there’s a context parameter (a standard Cocoa/Carbon callback pattern) that you could be using.

Separately, it might not be a bad idea to use FSPathCopyObjectAsync() instead of FSCopyObjectAsync(), since in Cocoa usually you have paths rather than FSRefs, and creating an FSRef requires going through -fileSystemRepresentation anyway. (Probably it would be the same code executed underneath, but it’s less to think about.)

Devon says:

Sorry for the stupid trackback thing, I’ve disabled it from my site.

[…] Cocoa Tutorial: File Copy With Progress Indicator […]

sqllyw says:

I tried this with tiger, it works with following minor change:

double val;
CFNumberGetValue(bytesCompleted, kCFNumberDoubleType, &val);
if (progressIndicator) {
[progressIndicator setDoubleValue:val];
[progressIndicator displayIfNeeded];
}

if you copy one big file, this works very well, but if you copy a batch of files, the display seems confusing as multiple files are being copied in the same time, how to schedule the copying in such a way that only one file is being copied at one time?