macosioappkitappstore-sandbox

Ask for the Documents permission in a sandboxed app


I am writing a Mac app in SwiftUI and would like to display a live-updated list of documents and folders with the ability to edit the files.

First, users selects any folder with Open File Dialog and then I save the URL into UserDefaults and attempt to read list of files and folders when the app launches.

Assuming I have a valid URL then I do the following:

// Open the folder referenced by URL for monitoring only.
monitoredFolderFileDescriptor = open(url.path, O_EVTONLY)

// Define a dispatch source monitoring the folder for additions, deletions, and renamings.
folderMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredFolderFileDescriptor, eventMask: .write, queue: folderMonitorQueue)

The app crashes when I call DispatchSource.makeFileSystemObjectSource with the EXC_BREAKPOINT exception.

FileManager.default.isReadableFile(atPath: url.path) returns false which tells me I don't have permissions to access this folder. The URL path is /Users/username/Documents/Folder

I have added NSDocumentsFolderUsageDescription into the info plist.

It's not clear how I can ask for permission programmatically. Theoretically, my URL can point to any folder on the file system that the user selects in the Open Dialog. It's unclear what is the best practice to request permission only when necessary. Should I parse the URL for the "Documents" or "Downloads" string? I also have watched this WWDC video.

Thanks for reading, here's an example what of what I am trying to show.

App would like to access files in your documents folder permissions dialog


Solution

  • Like @vadian said, this needs Secure Scoped Bookmark. If you user picks a folder from NSOpenPanel, permission dialog not necessary. This answer helped me a lot.

    I create new NSOpenPanel which gives me URL?, I pass this URL to the saveAccess() function below:

    let bookmarksPath = "bookmarksPath"
    var bookmarks: [URL: Data] = [:]
    
    func saveAccess(url: URL) {
        do {
            let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
            bookmarks[url] = data
            NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
        } catch {
            print(error)
        }
    }
    

    After you save bookmark, you can access when you app launches.

    func getAccess() {
        guard let bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data] else {
            print("Nothing here")
            return
        }
        guard let fileURL = bookmarks.first?.key else {
            print("No bookmarks found")
            return
        }
        guard let data = bookmarks.first?.value else {
            print("No data found")
            return
        }
        var isStale = false
        do {
            let newURL = try URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
            newURL.startAccessingSecurityScopedResource()
            print("Start access")
        } catch {
            print(error)
            return
        }
    }
    

    The code is very rough, but I hope it can help someone. After acquiring the newURL, you can print content of a folder.

    let files = try FileManager.default.contentsOfDirectory(at: newURL, includingPropertiesForKeys: [.localizedNameKey, .creationDateKey], options: .skipsHiddenFiles)
    

    And don't forget to call .stopAccessingSecurityScopedResource() when you done.