How to add ordered image uploads in WordPress for iOS 4.0.3

AppIcon76x76@2xI’ve finally figured out how to tweak WordPress for iOS so that images are uploaded in the correct order. I had been working on this fix for so long that the project has now changed and the file in question is no longer called in the current version of WordPress for iOS (4.1.3 at the time of writing).

The team are already working on a much better solution for inserting images, but until then the following works fine for me. Perhaps it’ll come in handy for future projects.

Before version 4.1.x, image uploads happened in a file called WPMediaUploader.m (found in Classes/Controllers/Media). In its uploadMediaObjects method, a for/in loop sends each object to an upload routine which inserts the relevant code into the post editor via a notification.

The trouble was that all uploads happen concurrently and hence the upload that finished first is inserted, neglecting the order in which the images were picked. Here’s the original method:

- (void)uploadMediaObjects:(NSArray *)mediaObjects
{
    if ([mediaObjects count] == 0)
        return;
    
    self.isUploadingMedia = YES;
    
    _numberOfImagesToUpload += [mediaObjects count];
    for (Media *media in mediaObjects) {
        [self uploadMedia:media];
    }
}

I tried a LOT to fix this, and the solution that works best is to wait for the first upload to finish and only then submit the next object to the upload queue. Here’s how I did this.

First I’ve added two new properties to the class: one holding the current array of media objects, and an int to count which object is currently uploading:

@property int currentUpload;
@property (nonatomic, strong) NSArray *mediaToUpload;

In the uploadMediaObjects method I’m storing this array in my property and start the first upload. I’m also incrementing the counter so we can keep track of all uploads in another method:

- (void)uploadMediaObjects:(NSArray *)mediaObjects
{
    if ([mediaObjects count] == 0)
        return;
    
    _numberOfImagesToUpload += [mediaObjects count];
    
    self.isUploadingMedia = YES;
    
    // store reference to media array
    self.mediaToUpload = mediaObjects;
    
    // observer for next media
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(uploadNext) name:@"UploadTheNextMediaThing" object:nil];
    
    // upload first object and increment counter
    [self uploadMedia:mediaObjects.firstObject];
    self.currentUpload++;

}

We also need a method that can be called via a notification when the current upload has finished:

- (void)uploadNext {
    
    // called via observer when current upload has finished
    
    // is another upload required?
    if (self.currentUpload < self.mediaToUpload.count) {
        
        Media *nextMedia = [self.mediaToUpload objectAtIndex:self.currentUpload];
        [self uploadMedia:nextMedia];
        self.currentUpload++;
    
    } else {
        [self cleanup];
    }
}

If another upload is required we’ll submit it, incrementing the counter. If not, we’ll reset those properties via a cleanup routine (more on that in a moment).

The observer that we’ve setup will be called after an upload, no matter if successful or not. For clarity I’ve added it twice (but of course it’s enough to add it once at the end of the method):

- (void)uploadMedia:(Media *)media
{
    [self uploadMedia:media withSuccess:^{
        if ([media isDeleted]) {
            DDLogWarn(@"Media uploader found deleted media while uploading (%@)", media);
            return;
        }
        [[NSNotificationCenter defaultCenter] postNotificationName:MediaShouldInsertBelowNotification object:media];
        [media save];
        NSLog(@"I've finished uploading image: %@", media.filename);
        
        // post notification that we're done uploading
        NSNotification *notification = [NSNotification notificationWithName:@"UploadTheNextMediaThing" object:self];
        [[NSNotificationCenter defaultCenter]postNotification:notification];
        
        
    } failure:^(NSError *error){
        if (error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled) {
            DDLogWarn(@"Media uploader failed with cancelled upload: %@", error.localizedDescription);
            return;
        }
        
        [WPError showAlertWithTitle:NSLocalizedString(@"Upload failed", nil) message:error.localizedDescription];
        
        // post notification that we're done uploading
        NSNotification *notification = [NSNotification notificationWithName:@"UploadTheNextMediaThing" object:self];
        [[NSNotificationCenter defaultCenter]postNotification:notification];
        
    }];

}

While I was testing this I’ve noticed that this class isn’t initialised again when you start another post, so our properties still contain (the wrong) values on our next post. Hence we need to do a bit of cleanup when we’re done by removing the observer and resetting our properties:

- (void)cleanup {
    
    // remove observer
    [[NSNotificationCenter defaultCenter]removeObserver:self name:@"UploadTheNextMediaThing" object:nil];
    
    // reset counter and media array
    self.currentUpload = nil;
    self.mediaToUpload = nil;
    
}

In case someone hits cancel during our uploads we must make sure we also call the above, otherwise we’ll experience another crash when we write the next post:

- (void)cancelAllUploads
{
    for (Media *media in _mediaUploads) {
        NSDictionary *uploadInformation = [_mediaUploads objectForKey:media];
        AFHTTPRequestOperation *operation = [uploadInformation objectForKey:WPMediaUploaderUploadOperation];
        [operation cancel];
    }
    [_mediaUploads removeAllObjects];
    self.isUploadingMedia = NO;
    [self cleanup];
}

And that’s it! I’ve created a fork the project with the above changes:

The “real” WordPress project is also on GitHub, fork away:

After WordPress 4.1 the project has been split into smaller chunks which will make it easier to integrate parts of WordPress into our own apps soon. See what else is available:





Leave a Reply