Cannot modify files outside my iOS app's sandbox. Permissions issue.

I'm developing an iOS app (latest Swift, iOS, Xcode versions atm of writing) which needs to rename audio files (and later modify its metadata) located anywhere (local storage, external storage, cloud, etc). I'm bringing my own question in StackOverflow but I'd like to know better from the source so I came here.

The trouble comes when testing on a physical device while trying to rename a file in some folder On My iPhone, the app does as intended when testing on the Simulator. On my physical iPhone I get an error logged saying that I don't have the permissions to do that.

“filename.mp3” couldn’t be moved because you don’t have permission to access “randomFolder".

I did my googling and asked GPTs about it, learned about FileCoordinator, UIDocumentPickerViewController⁠, startAccessingSecurityScopedResource. I also saw a couple of videos that I cannot reference in this forum.

Some code I have in place:

This is the document picker which I then call from a .sheet on another View.

struct DocumentPicker: UIViewControllerRepresentable {
    @Binding var newName: String
    @EnvironmentObject private var bookmarkController: BookmarkController
    
    func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
        let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes)
        documentPicker.delegate = context.coordinator
        return documentPicker
    }
    
    func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
        print("updateUIViewController documentPicker")
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self, newName)
    }

    class Coordinator: NSObject, UIDocumentPickerDelegate {
        var parent: DocumentPicker
        var newName: String
        
        init(_ parent: DocumentPicker, _ newName: String = "") {
            self.parent = parent
            self.newName = newName
        }
        
        func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
            // save bookmark
            print("documentPicker \(urls[0])")
            parent.bookmarkController.addBookmark(for: urls[0])
            
            // Rename the file
            var error: NSError?
            
            NSFileCoordinator().coordinate(readingItemAt: urls[0], options: [], error: &error) { coordinatedURL in
                do {
                    //                let data = try Data(contentsOf: newURL)
                    print("urls[0]: \(urls[0])")
                    print("coordinatedURL: \(coordinatedURL)")
                    print("renamedURL: \(newName)")
                    try renameFile(at: coordinatedURL, to: newName)
} catch  {
                    print("Error: \(error.localizedDescription)")
                }   
            }
        }
    }
}

Renaming function:


/// Rename selected file from browser
func renameFile(at fileURL: URL, to newName: String) throws {
    let fileExtension = fileURL.pathExtension
    let directory = fileURL.deletingLastPathComponent()

    // Create a new URL with the updated name and the original extension
    let renamedURL = directory.appendingPathComponent(newName).appendingPathExtension(fileExtension)

    try FileManager.default.moveItem(at: fileURL, to: renamedURL)
}

I have a BookmarkController in place so that my URLs are bookmarked for later use. Here it is:

import SwiftUI
import MobileCoreServices

class BookmarkController: ObservableObject {
    @Published var urls: [URL] = []
    
    init() {
        loadAllBookmarks()
    }
    
    func addBookmark(for url: URL) {
        print("adding bookmark for \(url)")
        do {
            guard url.startAccessingSecurityScopedResource() else {
                print("Failed to obtain access to the security-scoped resource.")
                return
            }
            
            defer { url.stopAccessingSecurityScopedResource() }
            
            let bookmarkData = try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil)
            
            let uuid = UUID().uuidString
            try bookmarkData.write(to: getAppSandboxDir().appendingPathComponent(uuid))
            
            urls.append(url)
        } catch {
            print("Error Adding Bookmark: \(error.localizedDescription)")
        }
    }
    
    func loadAllBookmarks() {
        // Get all the bookmark files
        let files = try? FileManager.default.contentsOfDirectory(at: getAppSandboxDir(), includingPropertiesForKeys: nil)
        // Map over the bookmark files
        self.urls = files?.compactMap { file in
            do {
                let bookmarkData = try Data(contentsOf: file)
                var isStale = false
                // Get the URL from each bookmark
                let url = try URL(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &isStale)
                
                guard !isStale else {
                    // Handle stale bookmarks
                    return nil
                }
                print("loaded bookmark: \(url)")
                // Return URL
                return url
            } catch {
                print("Error Loading Bookmark: \(error.localizedDescription)")
                return nil
            }
        } ?? []
    }
    
    private func getAppSandboxDir() -> URL {
        // TODO ver 0 index
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    }
}

When running on my device, I see url.startAccessingSecurityScopedResource throwing:

Failed to obtain access to the security-scoped resource.

I have also tried getting access to the parent directory like this:

let parentURL = url.deletingLastPathComponent()

and then using parentURL instead, but it also fails.


Something I noticed is that my app is not shown in the Settings -> Privacy & Security -> Files and Folders list. My app didn't trigger any dialog to be added here given user consent.

Fiddling with entitlements in Info.plist and Capabilities did not help either, but I'm not so sure about which ones should I add.

So:

Could it be that the problem I'm seeing about permissions and my app not appearing at the Files and Folders privacy settings on my iPhone are because my app is signed with a developer account which is not in the apple developer program? this is my last thought since I'm not understanding what else should I try.

Any pointers and help will be much appreciated. Thanks!

Answered by jaunmatrin in 777654022

I finally found a solution. According to this document, the user first needs to pick a folder to give the app access by returning a security-scoped URL (which you can then bookmark for later use) for it and all of its content. After the user picks the folder, they can do whatever they want on another picker for the specified file type.

It's still way too complicated for users to understand why they have to do this, so I made a user-friendly flow so that users are informed on how to continue. It still lacks some toast notifications for success but you'll get the idea. The code is on the repo I posted before.

If there's a way to make this in one step instead of having to prompt the user for a folder selection, it would be great. Please do let me know. Until then, this is what I could come up with.

You seem to be misunderstanding how bookmarks work.

This code is totally wrong:

let bookmarkData = try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil)
let uuid = UUID().uuidString
try bookmarkData.write(to: getAppSandboxDir().appendingPathComponent(uuid))

As is this:

let bookmarkData = try Data(contentsOf: file)
var isStale = false
// Get the URL from each bookmark
let url = try URL(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &isStale)

You can't use bookmarks this way. Bookmark data is used to provide an app with access to a file or resource which it would not normally have access to because of the sandbox. Bookmark data is specific to a launch of an app, so it will not survive quitting the app and re-launching it. For that, you need a security-scoped bookmark, but these are available only for macOS, and not on iOS.

Try something like this:

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
   urls.forEach { url in
        url.startAccessingSecurityScopedResource()
        defer { url.stopAccessingSecurityScopedResource() }

        // do whatever you need to do with each file
    }
}

Thank you for your support. I'm sure I must be misunderstanding everything :(

I tried your suggestion but it doesn't rename files on my phone's storage, still getting the permissions error. if security-scoped bookmarks are not available on iOS then I guess what I'm trying to do is not possible, making my app project not useful at all :'(

Is there a special entitlement I could get to be able to do this? The idea is that the app is a tool that can batch rename lots of files in external storage connected to the phone (and within the phone as well).

Here is a repo with a minimum reproducible example: https://github.com/juanmartin/renamerApp-ios

Thanks again.

Accepted Answer

I finally found a solution. According to this document, the user first needs to pick a folder to give the app access by returning a security-scoped URL (which you can then bookmark for later use) for it and all of its content. After the user picks the folder, they can do whatever they want on another picker for the specified file type.

It's still way too complicated for users to understand why they have to do this, so I made a user-friendly flow so that users are informed on how to continue. It still lacks some toast notifications for success but you'll get the idea. The code is on the repo I posted before.

If there's a way to make this in one step instead of having to prompt the user for a folder selection, it would be great. Please do let me know. Until then, this is what I could come up with.

Cannot modify files outside my iOS app's sandbox. Permissions issue.
 
 
Q