Search code examples
swiftmacosswiftuiappstore-sandbox

How to access (read-only) files under users selected directory?


SDK:macOS 14.0

Xcode: 15.0

I searched for a lot of answers, but it didn't work for my application. I can't access the contents of the files under the folder.

First of all, I configured permissions in App SandBox:

enter image description here

Then I used fileImporter in SwiftUI to allow users to select folders:

Button("open file")
.fileImporter(
    isPresented: $indexModel.openFileImporter,
    allowedContentTypes: [.movie, .avi, .mpeg4Movie, .directory],
    allowsMultipleSelection: true
) { results in
    do {
        let selection = try results.get()
        try self.openSelectedPath(selection)
    } catch let err {
        indexModel.errorMsg = err.localizedDescription
        indexModel.showError = true
    }
}

In openSelectedPath, I access the contents under the folder through FileManager:

if selection.hasDirectoryPath {
    let files = try FileManager.default.contentsOfDirectory(
        at: selection,
        includingPropertiesForKeys: nil,
        options: [.skipsHiddenFiles]
    )
    files.enumerated().filter { _, file in
        // try grant access here but all fails
        guard file.startAccessingSecurityScopedResource() else {
            print("\(file.lastPathComponent) cannot access")
            return false
        }
        let isMKV = file.pathExtension.compare("mkv", options: .caseInsensitive, range: nil, locale: nil) == .orderedSame
        return file.isFileURL && (isMKV || file.contains(.movie) || file.contains(.avi) || file.contains(.mpeg4Movie))
    }
    return
}

As a result, all the files in the folder are not accessible. (file.startAccessingSecurityScopedResourcereturn false)


Solution

  • Recently, I found solution inspired by the source code of iina/iina.

    1. Requesting access of the directory
    guard selection.startAccessingSecurityScopedResource() else {
        return
    }
    viewModel.activeURL = selection
    

    Keeping access right to this directory until you don't need it. For me, I did like this:

    class ViewModel: ObservableObject {
        var activeURL: URL? {
            didSet {
                oldValue?.stopAccessingSecurityScopedResource()
            }
        }
        
        deinit {
            activeURL?.stopAccessingSecurityScopedResource()
        }
    }
    
    1. Using FileManager.default.contentsOfDirectory(atPath: selection.path(percentEncoded: false)) instead of FileManager.default.contentsOfDirectory(at:selection,includingPropertiesForKeys: nil)

    2. Generating contents URL like this:

    let url = selection.appending(path: path, directoryHint: .notDirectory)