3

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.

  • There is no `FileManager` `exists(at: URL)` method. Can you show your implementation? – Leo Dabus Feb 21 '22 at 12:14
  • Whoops, forgot it was an extension, added to the question – Emanuele Tonetti Feb 21 '22 at 12:16
  • Note that the fileURL is a temporary file. You need to copy/move the file right away. It will be deleted as soon as that method finishes. Possible duplicate of [PDFView does not display my PDF](https://stackoverflow.com/a/48134186/2303865) – Leo Dabus Feb 21 '22 at 12:18
  • Not a duplicate, in that question the URL is valid from the start, but in my case the URL is already invalid when provided inside the scene(_, openUrlContexts) or in the application(_, open) methods, so I cannot copy the file away. Also, I'm not using a DocumentPicker. – Emanuele Tonetti Feb 21 '22 at 12:33
  • Your syntax is wrong. Should be `openURLContexts.first?.url`. Btw can you print its options as well `openURLContexts.first?.options`? – Leo Dabus Feb 21 '22 at 12:50
  • Options are: [__C.UIApplicationOpenURLOptionsKey(_rawValue: UIApplicationOpenURLOptionsSourceApplicationKey): com.apple.CoreSimulator.CoreSimulatorBridge, __C.UIApplicationOpenURLOptionsKey(_rawValue: UIApplicationOpenURLOptionsOpenInPlaceKey): 0]. – Emanuele Tonetti Feb 21 '22 at 12:52
  • Options in physical device, iOS 15: Optional() – Emanuele Tonetti Feb 21 '22 at 12:58

1 Answers1

5

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.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75