Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using NSProgress with nested NSOperations

I've been investigating NSProgress but have found the existing documentation, class reference and tutorials to be lacking. I'm mainly wondering if my NSProgress is applicable to my use case. The class reference documentation alternatively refers to suboperations or subtasks, I may be mistaken but I interpreted suboperations to mean a case where an NSOperation manages a group of other NSOperations. An example of my use case is as follows:

  • Create an Upload All Items in Group operation for each group that exists.
  • Add each of these operations to an NSOperationQueue.
  • Each Upload All Items in Group operation will create an Upload Item operation for each item in their group. These all get added to an NSOperationQueue managed by the operation.

I would have expected NSProgress to support this, and allow me to propagate progress from the nested operations (Upload Item operation) to the parent operation, and then finally to the main thread and the UI. But I've had difficulty implementing this, it seems as though NSProgress is meant more for long operations that execute all their code on one background thread, but have separate "sections" that make it easy to determine when progress has been made, if this is the case then the use of the term suboperation is a bit misleading as it brings to mind the use of nested NSOperations.

Thank you for any help you can provide, and let me know if additional details are needed.

like image 640
Arkcann Avatar asked Oct 04 '13 18:10

Arkcann


1 Answers

NSProgress knows nothing about NSOperations -- the two things are orthogonal -- but that doesn't mean it can't be used with them. The idea behind nesting NSProgress "tasks" is that the inner task doesn't know anything about the outer task, and the outer task doesn't need direct access to the inner task's NSProgress to pull in updates for it. I cooked up a little example:

// Outer grouping
NSProgress* DownloadGroupsOfFiles(NSUInteger numGroups, NSUInteger filesPerGroup)
{
    // This is the top level NSProgress object
    NSProgress* p = [NSProgress progressWithTotalUnitCount: numGroups];

    for (NSUInteger i = 0; i < numGroups; ++i)
    {
        // Whatever DownloadFiles does, it's worth "1 unit" to us.
        [p becomeCurrentWithPendingUnitCount: 1];

        DownloadFiles(filesPerGroup);

        [p resignCurrent];
    }

    return p;
}

// Inner grouping
void DownloadFiles(NSUInteger numberOfFiles)
{
    NSProgress* p = [NSProgress progressWithTotalUnitCount: numberOfFiles];
    NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];

    // Make the op queue last as long as the NSProgress
    objc_setAssociatedObject(p, NULL, opQueue, OBJC_ASSOCIATION_RETAIN);

    // For each file...
    for (NSUInteger i = 0; i < numberOfFiles; ++i)
    {
        // Whatever this DownloadOperation does is worth 1 "unit" to us.
        [p becomeCurrentWithPendingUnitCount: 1];

        // Make the new operation
        MyDownloadOperation* op = [[MyDownloadOperation alloc] initWithName: [NSString stringWithFormat: @"File #%@", @(i+1)]];
        [opQueue addOperation: op];

        [p resignCurrent];
    }
}

// And then the DownloadOperation might look like this...
@interface MyDownloadOperation : NSOperation
@property (nonatomic, readonly, copy) NSString* name;
- (id)initWithName: (NSString*)name;
@end

@implementation MyDownloadOperation
{
    NSProgress* _progress;
    NSString* _name;
}

- (id)initWithName:(NSString *)name
{
    if (self = [super init])
    {
        _name = [name copy];
        // Do this in init, so that our NSProgress instance is parented to the current one in the thread that created the operation
        _progress = [NSProgress progressWithTotalUnitCount: 1];
    }
    return self;
}

- (void)dealloc
{
    _name = nil;
    _progress = nil;
}

- (void)main
{
    // Fake like we're doing something that takes some time

    // Determine fake size -- call it 768K +- 256K
    const NSUInteger size = 512 * 1024 + arc4random_uniform(512*1024);
    const NSUInteger avgBytesPerSec = 1024 * 1024;
    const NSTimeInterval updatePeriod = 1.0/60.0;

    // Make sure all the updates to the NSProgress happen on the main thread
    // in case someone is bound to it.
    dispatch_async(dispatch_get_main_queue(), ^{
        _progress.totalUnitCount = size;
        _progress.completedUnitCount = 0;
    });

    NSUInteger bytesRxd = 0;
    do
    {
        // Sleep for a bit...
        usleep(USEC_PER_SEC * updatePeriod);

        // "Receive some data"
        NSUInteger rxdThisTime = updatePeriod * avgBytesPerSec;

        // Never report more than all the bytes
        bytesRxd = MIN(bytesRxd + rxdThisTime, size);

        // Update on the main thread...
        dispatch_async(dispatch_get_main_queue(), ^{
            [_progress setCompletedUnitCount: bytesRxd];
        });
    } while (bytesRxd < size);
}

@end

One thing to note is that if NSProgress is being used to convey status to the UI, then you will want to make sure that every time you update the NSProgress object, you do so from the main thread, otherwise you'll get lots of weird crashes.

Alternately you could just use NSURLConnection to download files, and then have a delegate like this:

@interface MyURLConnectionProgressReporter : NSObject <NSURLConnectionDownloadDelegate>
@property (nonatomic, readwrite, assign) id<NSURLConnectionDownloadDelegate> delegate;
@end

NSProgress* DownloadABunchOfFiles(NSArray* arrayOfURLs)
{
    arrayOfURLs = arrayOfURLs.count ? arrayOfURLs : @[ [NSURL URLWithString: @"http://www.google.com"] ];

    NSProgress* p = [NSProgress progressWithTotalUnitCount: arrayOfURLs.count];

    for (NSURL* url in arrayOfURLs)
    {
        [p becomeCurrentWithPendingUnitCount: 1];

        MyURLConnectionProgressReporter* delegate = [[MyURLConnectionProgressReporter alloc] init];
        NSURLConnection* conn = [[NSURLConnection alloc] initWithRequest: [NSURLRequest requestWithURL: url] delegate: delegate];
        [conn start];

        [p resignCurrent];
    }

    return p;

}

@implementation MyURLConnectionProgressReporter
{
    NSProgress* _progress;
}

static void EnsureMainThread(dispatch_block_t block);

- (id)init
{
    if (self = [super init])
    {
        _progress = [NSProgress progressWithTotalUnitCount: 1];
        EnsureMainThread(^{
            _progress.kind = NSProgressKindFile;
            [_progress setUserInfoObject:NSProgressFileOperationKindDownloading forKey:NSProgressFileOperationKindKey];
        });
    }
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    id retVal = [super forwardingTargetForSelector:aSelector];
    if (!retVal && [self.delegate respondsToSelector: _cmd])
    {
        retVal = self.delegate;
    }
    return retVal;
}

- (void)p_updateWithTotalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes
{
    // Update our progress on the main thread...
    EnsureMainThread(^{
        if (!expectedTotalBytes)
            _progress.totalUnitCount = -1;
        else
            _progress.totalUnitCount = MAX(_progress.totalUnitCount, expectedTotalBytes);

        _progress.completedUnitCount = totalBytesWritten;
    });
}

- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes
{
    // Update our progress
    [self p_updateWithTotalBytesWritten: totalBytesWritten expectedTotalBytes: expectedTotalBytes];

    // Then call on through to the other delegate
    if ([self.delegate respondsToSelector: _cmd])
    {
        [self.delegate connection:connection didWriteData:bytesWritten totalBytesWritten:totalBytesWritten expectedTotalBytes:expectedTotalBytes];
    }
}

- (void)connectionDidResumeDownloading:(NSURLConnection *)connection totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes
{
    // Update our progress
    [self p_updateWithTotalBytesWritten: totalBytesWritten expectedTotalBytes: expectedTotalBytes];

    // Then call on through to the other delegate
    if ([self.delegate respondsToSelector: _cmd])
    {
        [self.delegate connectionDidResumeDownloading:connection totalBytesWritten:totalBytesWritten expectedTotalBytes:expectedTotalBytes];
    }
}

- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL
{
    // We're done, so we want (_progress.completedUnitCount == _progress.totalUnitCount)
    EnsureMainThread(^{
        _progress.completedUnitCount = _progress.totalUnitCount;
    });

    if ([self.delegate respondsToSelector: _cmd])
    {
        [self.delegate connectionDidFinishDownloading:connection destinationURL:destinationURL];
    }
}

static void EnsureMainThread(dispatch_block_t block)
{
    if (!block)
        return;
    else if ([NSThread isMainThread])
        block();
    else
        dispatch_async(dispatch_get_main_queue(), block);
}

@end

Hope that helps.

like image 199
ipmcc Avatar answered Sep 21 '22 22:09

ipmcc