-
Notifications
You must be signed in to change notification settings - Fork 0
URL
URL is basically a value wrapper for NSURL, but despite nearly-identical interfaces, the semantics are different! So be careful translating your code:
URL(fileURLWithPath: "")
-> URL = "./ -- ile:///Users/ken/"
NSURL(fileURLWithPath: "") == nil
-> Bool = true
(Yes, "ile" is not a typo on this page. It really says that. Blame LLVM.)
URL/NSURL has a predicate hasDirectoryPath
. The Swift name is described as "A Boolean that is true if the URL path represents a directory", while the Objective-C variant has no documentation. You'd never guess it from the name or documentation, but all this does is check if it ends with a /
character. If you want to know if it's actually a directory, use FileManager.
Security Scoped Bookmarks are (like the rest of the Mac sandbox) poorly documented. It's especially troublesome here because if you don't get everything right, it won't throw an error, or return nil, or crash, or show a compiler warning or error. It'll work sometimes. The failure mode tends to be "always return success, but sometimes return no data".
Here's all of the things you need to get right to make it work reliably:
-
Add
com.apple.security.files.bookmarks.app-scope
(or.document-scope
) to your Info.plist! This one isn't a checkbox in the Capabilities view, so you have to type it yourself. (But this blog post says the former one is not required, so take your pick.) -
Pass
options: [.withSecurityScope]
, both toURL(resolvingBookmarkData:...)
and.bookmarkData(...)
. (These are in fact different values, of different types:NSURL.BookmarkResolutionOptions
versusNSURL.BookmarkCreationOptions
. But in Swift, the enclosing type is inferred from context, so you can disregard this fact, since they have the same name.) -
Note that while URL is a value type, it's (apparently) backed by more than just the string representation. In particular, you must call
startAccessingSecurityScopedResource()
on the URL instance created byURL(resolvingBookmarkData:...)
, and not another URL created with the same file path. (If you violate this, you'll find thatstartAccessingSecurityScopedResource()
always returnsfalse
.)
You should call startAccessingSecurityScopedResource()
on a decoded URL bookmark, and stopAccessingSecurityScopedResource()
when you're done. Unfortunately, there's a ton of ambiguity around this.
Do these calls nest? You might think so, from reading the Core Foundation API documentation: "You must balance every call to the CFURLStartAccessingSecurityScopedResource(_:)
method with a corresponding call to the CFURLStopAccessingSecurityScopedResource(_:)
method." But the App Sandbox Design Guide clarifies: "Calls to start and stop access are not nested. When you call the stopAccessingSecurityScopedResource method, you immediately lose access to the resource. If you call this method on a URL whose referenced resource you do not have access to, nothing happens."
- It's not clear if calling start multiple times is a leak, or a no-op, or an exception, or what. It doesn't seem to hurt anything, but given PowerBox's failure modes, I wouldn't risk it.
I recommend creating a (GCD-synchronized) collection of URLs, to manually keep track of what locations you've started/stopped. There's no built-in interface for figuring out what resources you have access to, so you need to track this somehow, anyway.
CHECK: If you call startAccessingSecurityScopedResource()
and it returns false
, it's not clear what this means. The documentation only says the request didn't succeed, and you must call stopAccessingSecurityScopedResource()
if it did succeed. One blog post suggested that it can return false
for URLs which are already part of the sandbox, which would mean you can safely ignore it (and since calling "stop" on unknown URLs is safe, you don't even need to record that it failed).
URL init has an isStale
out param, which is described in one place as "Your app should create a new bookmark using the returned URL and use it in place of any stored copies of the existing bookmark". I've never seen this happen, and I don't know how to cause it to happen, so I can't run a test to figure out what exactly this means.
- One blog I found suggests it can't happen.
- One blog I found suggests it means PowerBox no longer encompasses this URL, so you also need to prompt the user for intent again.
Who knows? Personally, I just os_log
it and remove it from the list.
CHECK: I heard of IRC that if a URL points to a network disk, macOS will try to mount it (big latency). If the disk is not available yet, you get a URL but when the disk is finally mounted, the URL won't point to a usable file (so "resolve" more lazily).