SwiftUI fileImporter vs dropDestination logic

If I drag something into my SwiftUI Mac app the .dropDestination gets an array of URLs that I can do with what I want.

If I use .fileImporter to get an identical array of URLs I should wrap start/stop securityScopedResource() calls around each URL before I do anything with it.

Can anyone explain the logic behind that? Is there some reason I'm not seeing? It is especially annoying in that the requirement for security scoping also doesn't exist if I use an NSOpenPanel instead of .fileImporter.

Replies

The relationship between security-scoped URLs and the document pickers is a complex one. My general advice is that you call {start,Stop}SecurityScopedResource on any URL that you get from ‘outside’. It doesn’t do anything bad if you call it redundantly [1].

Share and Enjoy

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

[1] Consider this routine:

func test(_ url: URL) {
    print("will test")
    
    let sA = try? String(contentsOf: url, encoding: .utf8)
    print("before outer start, sA: '\(sA ?? "-")'")

    let didStartB = url.startAccessingSecurityScopedResource()
    let sB = try? String(contentsOf: url, encoding: .utf8)
    print("after outer start, didStart: \(didStartB), sB: '\(sB ?? "-")'")

    let didStartC = url.startAccessingSecurityScopedResource()
    let sC = try? String(contentsOf: url, encoding: .utf8)
    print("after inner started, didStart: \(didStartC), sC: '\(sC ?? "-")'")
    if didStartC {
        url.stopAccessingSecurityScopedResource()
    }

    let sD = try? String(contentsOf: url, encoding: .utf8)
    print("after inner stop, sD: '\(sD ?? "-")'")

    if didStartB {
        url.stopAccessingSecurityScopedResource()
    }

    let sE = try? String(contentsOf: url, encoding: .utf8)
    print("after outer stop, sE: '\(sE ?? "-")'")

    print("did test")
}

If you call it with a URL from .fileImporter(…), you get this result:

will test
before outer start, sA: '-'
after outer start, didStart: true, sB: 'Hello Cruel World!'
after inner started, didStart: true, sC: 'Hello Cruel World!'
after inner stop, sD: 'Hello Cruel World!'
after outer stop, sE: '-'
did test

Sounds simple. Except when the user opens a folder and the app only cares about the contents of the folder. Calling start/stop on the contents may not hurt, but my testing says it does not help. Apparently I must call start/stop on the folder to access the contents later in the program. But only for fileImporter. I don't need to do that for dragged folders or folders opened with NSOpenPanel.

I have a solution that works for my app, but it seems strange that I need to go through extra steps on some input (fileImporter), but not others (drag).

Anyway, thanks for your reply.

Sounds simple.

Nothing about this stuff is simple O-:

But only for .fileImporter(…). I don't need to do that for dragged folders or folders opened with NSOpenPanel.

That’s interesting, and it’s hard to understand without a historical perspective.

Note I’m going to assume terms from On File System Permissions here.

When we first implemented App Sandbox we made the decision to ‘auto start’ the URLs coming in via the standard file panels, drag’n’drop, and so on. So, you don’t have to call {start,Stop}AccessingSecurityScopedResource() on those. I’m not sure why we did this, but I suspect it was choice to promote source code compatibility. That is, we wanted to make it easier for folks to adopt App Sandbox.

However, that choice has a number of downsides. Most notably, this auto start behaviour effectively leaks sandbox extension. This leads to a long-standing bug in sandboxed apps: If you select thousands of files in the open panel, things fail badly.

It also presents problems for the cross-platform .fileImporter(…) API. On macOS, should it follow the behaviour of the open panel? Or should it be consistent with the other SwiftUI platforms? Clearly the SwiftUI folks chose the latter, and it’s hard to fault them for that.

On the plus side, this auto start helps with MAC. MAC reuses a bunch of sandbox infrastructure. However, it needs binary compatibility. So, unlike App Sandbox, it can’t require developers to change their code.

Share and Enjoy

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

I have a follow up question on this topic. According to the documentation, startAccessingSecurityScopedResource() "returns true if the request to access the resource succeeded; otherwise, false"

This works fine for when called from .fileImporter. However when called from .dropDestination(for: URL.self) { urls, _ in ... then url.startAccessingSecurityScopedResource()will always return false. I'm importing the same files using these two methods.

if url.startAccessingSecurityScopedResource() == true {            
            let data = try Data(contentsOf: url) // works for .fileImporter
            url.stopAccessingSecurityScopedResource()
} else {
            throw EmbeddingsError.noAccess // but calls from .dropDestination(for: URL.self) { urls, _ in end up here
}

What does work is to ignore the return value of url.startAccessingSecurityScopedResource() like so:

_ = url.startAccessingSecurityScopedResource()  
let data = try Data(contentsOf: url) // works for .fileImporter
url.stopAccessingSecurityScopedResource()

But that can't be the way it's supposed to work. Is there anything I'm missing?