Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Open In" with dynamically generated file fails in iOS 8

I use this code to save some PDF data to a file, send it to another app using the "Open In" menu, then delete the file when that's done:

- (void)openIn:(NSData *)fileData {
    // save the PDF data to a temporary file
    NSString *fileName = [NSString stringWithFormat:@"%@.pdf", self.name];
    NSString *filePath = [NSString stringWithFormat:@"%@/Documents/%@", NSHomeDirectory(), fileName];
    BOOL result = [fileData writeToFile:filePath atomically:TRUE];
    if (result) {
        NSURL *URL = [NSURL fileURLWithPath:filePath];
        UIDocumentInteractionController *controller = [[UIDocumentInteractionController interactionControllerWithURL:URL] retain];
        controller.delegate = self;
        [controller presentOpenInMenuFromBarButtonItem:self.openInButton animated:TRUE];
    }
}

- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller {
    // when the document interaction controller finishes, delete the temporary file
    NSString *fileName = [NSString stringWithFormat:@"%@.pdf", self.name];
    NSString *filePath = [NSString stringWithFormat:@"%@/Documents/%@", NSHomeDirectory(), fileName];
    [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}

This has worked fine until iOS 8. Now, the file is created and I can verify that it contains the correct content, the Open In menu appears, I can select an app, and the delegate method runs and cleans up the file. But instead of iOS switching to the selected app and copying the file into it as it did before, the Open In menu simply closes when I select an app, and the file is not copied.

This works if I give the UIDocumentInteractionController an existing file. It also works if I use the provided fileData but change the destination filename to the filename of an existing file. This suggests a permissions problem -- as if new files are created in iOS 8 with default permissions that UIDocumentInteractionController can't read.

Does anyone know what's happening and how I can work around it?

like image 795
arlomedia Avatar asked Jan 09 '23 16:01

arlomedia


1 Answers

It looks like the order of operations has changed slightly in iOS 8. DidDismissOpenInMenu used to run after the file was finished sending, but now it runs after the file begins sending. This means my cleanup code was sometimes running before the file was finished sending, leaving no file to send. I figured this out after noticing that smaller files were being sent okay; apparently the processing for smaller files was finishing before my cleanup code got them, but the processing for larger files was not.

To ensure the correct timing, but also clean up files that are created when the user opens the DocumentInteractionController and then dismisses the controller without doing anything, I changed my methods like this:

- (void)openIn:(NSData *)fileData {
    // save the PDF data to a temporary file
    NSString *fileName = [NSString stringWithFormat:@"%@.pdf", self.name];
    NSString *filePath = [NSString stringWithFormat:@"%@/Documents/%@", NSHomeDirectory(), fileName];
    BOOL result = [fileData writeToFile:filePath atomically:TRUE];
    if (result) {
        self.sendingFile = FALSE;
        NSURL *URL = [NSURL fileURLWithPath:filePath];
        UIDocumentInteractionController *controller = [[UIDocumentInteractionController interactionControllerWithURL:URL] retain];
        controller.delegate = self;
        [controller presentOpenInMenuFromBarButtonItem:self.openInButton animated:TRUE];
    }
}

- (void)documentInteractionController:(UIDocumentInteractionController *)controller willBeginSendingToApplication:(NSString *)application {
    // the user chose to send the file, so we shouldn't clean it up until that's done
    self.sendingFile = TRUE;
}

- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller {
    if (!self.sendingFile) {
        // the user didn't choose to send the file, so we can clean it up now
        [self openInCleanup];
    }
}

- (void)documentInteractionController:(UIDocumentInteractionController *)controller didEndSendingToApplication:(NSString *)application {
    // the user chose to send the file, and the sending is finished, so we can clean it up now
    [self openInCleanup];
    self.sendingFile = FALSE;
}

- (void)openInCleanup {
    // delete the temporary file
    NSString *fileName = [NSString stringWithFormat:@"%@.pdf", self.name];
    NSString *filePath = [NSString stringWithFormat:@"%@/Documents/%@", NSHomeDirectory(), fileName];
    [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}

Update for iOS 11

Before iOS 11, it seems that the operating system kept a copy of the file available until the receiving app was finished reading it, even though my cleanup function ran as soon as the file was sent out from my app. In iOS 11, this changed and the receiving app fails to read the file because my app deletes it before that's done. So now instead of saving the temporary file to Documents and using the openInCleanup method to delete it immediately, I'm saving the temporary file to tmp and emptying the tmp folder next time the app launches. This approach should also work with older iOS versions. Just remove openInCleanup, change Documents to tmp in the paths, and add this to applicationDidFinishLaunching:

// clear the tmp directory, which will contain any files saved for Open In
NSString *tmpDirectory = [NSString stringWithFormat:@"%@/tmp", NSHomeDirectory()];
NSArray *tmpFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tmpDirectory error:NULL];
for (NSString *tmpFile in tmpFiles) {
    [[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:@"%@/%@", tmpDirectory, tmpFile] error:NULL];
}
like image 184
arlomedia Avatar answered Jan 23 '23 23:01

arlomedia