I'm developing an iOS routing app based on MapKit where I've implemented support for files written in the GPX open standard (essentially XML) to codify all user's info about routing, which can be either exported or imported for backup purposes.
When the user is importing back the GPX file in the app, I've made so that he can do it through the Files app by sharing the file to the app. When doing so, AppDelegate or SceneDelegate intercepts the file URL depending on the current iOS version:
In AppDelegate (<= 12.4):
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// Handle the URL in the url variable
}
In SceneDelegate (>= 13):
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
// Handle the URL in the URLContexts.first?.url
}
In my Info.plist the flags LSSupportsOpeningDocumentsInPlace and UIFileSharingEnabled are both set to YES. There's an UTImportedTypeDeclarations that lists the .gpx extension so that the Files app automatically selects my app in order to open it.
This method worked effortlessly several months ago and both the path and URLContexts.first?.url variables were populated and you could access the file pointed by the URL in place, read it and parse its routing data without copying it around or doing any special handling. That url was then sent to an appropriate ViewController for the user to review, all good and fine.
Fast forward to now after receiving a ticket, GPX import is broken. Debugging revealed that when in iOS the user sends the app a GPX file, either AppDelegate or SceneDelegate receive an URL that seems to be correct, but the file pointed by the URL straight up doesn't exists according to FileManager. Using this extension:
extension FileManager {
public func exists(at url: URL) -> Bool {
let path = url.path
return fileExists(atPath: path)
}
}
In AppDelegate/SceneDelegate:
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
FileManager.default.exists(at: url) // Result: false
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let url = URLContexts.first?.url {
FileManager.default.exists(at: url) // Result: false
}
}
The source of the file doesn't matter, as sending it from an iCloud or from a local folder doesn't change the outcome. Using a physical device or the emulator has the same result. Why the URL is invalid? Is this a new system bug? Or, is this even the proper way to receive files in iOS by the Files app? Was this method deprecated in the new iOS 15? Or am I just forgetting something crucial?
Thanks for the patience.
CodePudding user response:
Found what's wrong. When you open a file from the share sheet of another app and the LSSupportsOpeningDocumentsInPlace is TRUE, before you can access it from the URL object you must call url.startAccessingSecurityScopedResource() so you are granted access the file. When you're done, you must follow with a stopAccessingSecurityScopedResource() call on the same URL object. Example below uses defer keyword to ensure that the closing method is always called.
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if url.startAccessingSecurityScopedResource() {
defer {
url.stopAccessingSecurityScopedResource()
}
FileManager.default.exists(at: url) // Result: true
}
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let url = URLContexts.first?.url, url.startAccessingSecurityScopedResource() {
defer {
url.stopAccessingSecurityScopedResource()
}
FileManager.default.exists(at: url) // Result: true
}
}
This answer was taken from https://stackoverflow.com/a/62771024/18267710.