iOS Creating a file in a public folder

Hello there. I want to create a file in a public folder (where it could be seen by user, and the file could be read again.

To achieve that, I use document picker, and set the document type to "public.folder" and inMode "UIDocumentPickerModeOpen".

After user opens the document picker and selects the desired folder, in "didPickDocumentsAtURLs" callback I get the NSUrl object, which has permissions to modify the file at that url (in this case, it's an url to a folder).

There is my issue. I have the url with access permission to a folder, however, to create a file I ussualy need to have the filename.extension in the url. If I were to modify the NSUrl object I've received from the document picker, or convert it to NSString, my guess is I lose the access permission and "createFileAtPath" method always fails.

Any leads would be kindly appreciated!

Answered by Dielektrik in 682427022

Hello eskimo!

Thank you for the advices, they are super helpful and I'll be sure to implement them in my code!

However, I won't accept your answer, since the issue in my case was, that I was trying to call startAccessingSecurityScopedResource with a modified url (the folder that the user chose + NewFileName.extension). It kept returning false, no matter how I've modified the URL.

What worked for me, was calling startAccessingSecurityScopedResource with an unmodified folder url, that I receive from document picker.

Here I will leave a fully working example for future readers, to make their life easier:

- (void)openDocumentPicker
{
    //This is needed, when using this code on QT!
    //Find the current app window, and its view controller object
    /*
    UIApplication* app = [UIApplication sharedApplication];
    UIWindow* rootWindow = app.windows[0];
    UIViewController* rootViewController = rootWindow.rootViewController;
     */
    
    //Initialize the document picker. Set appropriate document types
    //When reading: use document type of the file, that you're going to read
    //When writing into a new file: use @"public.folder" to select a folder, where your new file will be created
    UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.folder"] inMode:UIDocumentPickerModeOpen];
    
    //Assigning the delegate, connects the document picker object with callbacks, defined in this object
    documentPicker.delegate = self;

    documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;

    //In this case we're using self. If using on QT, use the rootViewController we've found before
    [self presentViewController:documentPicker animated:YES completion:nil];
}

- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
{
    //If we come here, user successfully picked a single file/folder
    
    //When selecting a folder, we need to start accessing the folder itself, instead of the specific file we're going to create
    if ( [urls[0] startAccessingSecurityScopedResource] ) //Let the os know we're going use this resource
    {
        //Write file case ---
        
        //Construct the url, that we're going to be using: folder the user chose + add the new FileName.extension
        NSURL *destURLPath = [urls[0] URLByAppendingPathComponent:@"Test.txt"];
        
        NSString *dataToWrite = @"This text is going into the file!";
        
        NSError *error = nil;
        
        //Write the data, thus creating a new file. Save the new path if operation succeeds
        if( ![dataToWrite writeToURL:destURLPath atomically:true encoding:NSUTF8StringEncoding error:&error] )
            NSLog(@"%@",[error localizedDescription]);

        
        //Read file case ---
        NSData *fileData = [NSData dataWithContentsOfURL:destURLPath options:NSDataReadingUncached error:&error];
        
        if( fileData == nil )
            NSLog(@"%@",[error localizedDescription]);
        
        [urls[0] stopAccessingSecurityScopedResource];
    }
    else
    {
        NSLog(@"startAccessingSecurityScopedResource failed");
    }
}

If your process is granted a sandbox extension to modify a folder then that extension covers any items within that folder. So, you should be able to do something like this:

let dir: URL = …
let file = dir.appendingPathComponent("test.txt")
let success = dir.startAccessingSecurityScopedResource()
guard success else { … }
defer { dir.stopAccessingSecurityScopedResource() }
let content = Data("Hello Cruel World!".utf8)
try content.write(to: file, options: [])

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Here's the code:

NSError *error = nil;
NSString* data = @"This data going into the file";

NSURL *destURLPath = [NSURL URLWithString:urls[0].absoluteString];
NSURL *destURLPath2 = [destURLPath URLByAppendingPathComponent:@"testText.txt"];

[destURLPath2 startAccessingSecurityScopedResource]; //Let the os know we're going to read the file

//destURLPath2 = file:///private/var/mobile/Containers/Shared/AppGroup/C9D2B2AF-4086-45BF-98C6-B0BEBA599FA4/File%20Provider%20Storage/New/testText.txt

if( [data writeToURL:destURLPath2 atomically:true encoding:NSUTF8StringEncoding error:&error] == NO )
    NSLog(@"Error: %@ %@", error, [error userInfo]);

[destURLPath2 stopAccessingSecurityScopedResource];

Yes indeed, your code in swift works like a charm!

Cool.

However, I didn't mention before, but my project is in objective-c.

There’s nothing Swift specific about the techniques I’m describing; they should all work just fine in Objective-C.

As to what’s going on here, I see that you’re round tripping your URLs through NSString. This is likely to cause problems because a security-scoped URL contains extra info, the security scope itself, that’s easily dropped during that round trip. I recommend that you tweak your code to use URLs everywhere.

Also, check the function result from -startAccessingSecurityScopedResource. I suspect you’ll find that it’s returning NO, which is a clear indication of this problem. However, you must check this result in production code too; just because the security scope in the URL was once valid does not mean that it’ll be valid forever.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Hello eskimo!

Thank you for the advices, they are super helpful and I'll be sure to implement them in my code!

However, I won't accept your answer, since the issue in my case was, that I was trying to call startAccessingSecurityScopedResource with a modified url (the folder that the user chose + NewFileName.extension). It kept returning false, no matter how I've modified the URL.

What worked for me, was calling startAccessingSecurityScopedResource with an unmodified folder url, that I receive from document picker.

Here I will leave a fully working example for future readers, to make their life easier:

- (void)openDocumentPicker
{
    //This is needed, when using this code on QT!
    //Find the current app window, and its view controller object
    /*
    UIApplication* app = [UIApplication sharedApplication];
    UIWindow* rootWindow = app.windows[0];
    UIViewController* rootViewController = rootWindow.rootViewController;
     */
    
    //Initialize the document picker. Set appropriate document types
    //When reading: use document type of the file, that you're going to read
    //When writing into a new file: use @"public.folder" to select a folder, where your new file will be created
    UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.folder"] inMode:UIDocumentPickerModeOpen];
    
    //Assigning the delegate, connects the document picker object with callbacks, defined in this object
    documentPicker.delegate = self;

    documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;

    //In this case we're using self. If using on QT, use the rootViewController we've found before
    [self presentViewController:documentPicker animated:YES completion:nil];
}

- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
{
    //If we come here, user successfully picked a single file/folder
    
    //When selecting a folder, we need to start accessing the folder itself, instead of the specific file we're going to create
    if ( [urls[0] startAccessingSecurityScopedResource] ) //Let the os know we're going use this resource
    {
        //Write file case ---
        
        //Construct the url, that we're going to be using: folder the user chose + add the new FileName.extension
        NSURL *destURLPath = [urls[0] URLByAppendingPathComponent:@"Test.txt"];
        
        NSString *dataToWrite = @"This text is going into the file!";
        
        NSError *error = nil;
        
        //Write the data, thus creating a new file. Save the new path if operation succeeds
        if( ![dataToWrite writeToURL:destURLPath atomically:true encoding:NSUTF8StringEncoding error:&error] )
            NSLog(@"%@",[error localizedDescription]);

        
        //Read file case ---
        NSData *fileData = [NSData dataWithContentsOfURL:destURLPath options:NSDataReadingUncached error:&error];
        
        if( fileData == nil )
            NSLog(@"%@",[error localizedDescription]);
        
        [urls[0] stopAccessingSecurityScopedResource];
    }
    else
    {
        NSLog(@"startAccessingSecurityScopedResource failed");
    }
}
iOS Creating a file in a public folder
 
 
Q