Files App Share Context with Security scoped resource fails

I'm creating an App that can accepted PDFs from a shared context. I am using iOS, Swift, and UIKit with IOS 17.1+

The logic is:

  • get the context
  • see who is sending in (this is always unknown)
  • see if I can open in place (in case I want to save later)
  • send the URL off to open the (PDF) document and
  • load it into PDFKit's pdfView.document

I have no trouble loading PDF docs with the file picker.

And everything works as expected for shares from apps like Messages, email, etc... (in which case URLContexts.first.options.openInPlace == False)

The problem is with opening (sharing) a PDF that is sent from the Files App. (openInPlace == True)

If the PDF is in the App's Document Folder, I need the Security scoped resource, to access the URL from the File's App so that I can copy the PDF's data to the PDFViewer.document. I get Security scoped resource access granted each time I get the File App's context URL. But, when I call fileCoordinator.coordinate and try to access a file outside of the App's document folder using the newUrl, I get an error.

FYI - The newUrl (byAccessor) and context url (readingItemAt) paths are always same for the Files App URL share context.

I can, however, copy the file to a new location in my apps directory and then open it from there and load in the data. But I really do not want to do that.

. . . . .

Questions:

Am I missing something in my pList or are there other parameters specific to sharing a file from the Files App?

I'd appreciate if someone shed some light on this?

. . . . .

Here are the parts of my code related to this with some print statements...

. . . . .

SceneDelegate

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    
    // nothing to see here, move along
    guard let urlContext = URLContexts.first else {
        print("No URLContext found")
        return
    }
    
    // let's get the URL (it will be a PDF)
    let url = urlContext.url
    let openInPlace = urlContext.options.openInPlace
    let bundleID = urlContext.options.sourceApplication
    print("Triggered with URL: \(url)")
    print("Can Open In Place?: \(openInPlace)")
    print("For Bundle ID:      \(bundleID ?? "None")")
    
    // get my Root ViewController from window
    if let rootViewController = self.window?.rootViewController {
        
        // currently using just the view
        if let targetViewController = rootViewController as? ViewController {
            targetViewController.prepareToLoadSharedPDFDocument(at: url)
        }

        // I might use a UINavigationController in the future
        else if let navigationController = rootViewController as? UINavigationController,
                let targetViewController = navigationController.viewControllers.first as? ViewController {
            targetViewController.prepareToLoadSharedPDFDocument(at: url)
        }
    }
    
}

. . . .

ViewController function

I broke out the if statement for accessingScope just to make it easier for me the debug and play around with the code in accessingScope == True

    func loadPDF(fromUrl url: URL) {
        
        // If using the File Picker / don't use this        
        // If going through a Share.... we pass the URL and have three outcomes (1, 2a, 2b)
        
        // 1. Security scoped resource access NOT needed if from a Share Like Messages or EMail
        
        // 2. Security scoped resource access granted/needed from 'Files' App
        //    a. success if in the App's doc directory
        //    b. fail if NOT in the App's doc directory

        // Set the securty scope variable
        var accessingScope = false
        
        // Log the URLs for debugging
        print("URL String: \(url.absoluteString)")
        print("URL Path:   \(url.path())")
        
        // Check if the URL requires security scoped resource access
        if url.startAccessingSecurityScopedResource() {
            accessingScope = true
            print("Security scoped resource access granted.")
        } else {
            print("Security scoped resource access denied or not needed.")
        }
        
        // Stop accessing the scope once everything is compeleted
        defer {
            if accessingScope {
                url.stopAccessingSecurityScopedResource()
                print("Security scoped resource access stopped.")
            }
        }
        
        // Make sure the file is still there (it should be in this case)
        guard FileManager.default.fileExists(atPath: url.path) else {
            print("File does not exist at URL: \(url)")
            return
        }
        
        // Let's see if we can open it in place
        
        if accessingScope {
            
            let fileCoordinator = NSFileCoordinator()
            var error: NSError?
            fileCoordinator.coordinate(readingItemAt: url, options: [], error: &error) { (newUrl) in
                DispatchQueue.main.async {
                    print(url.path())
                    print(newUrl.path())
                    if let document = PDFDocument(url: newUrl) {
                        self.pdfView.document = document
                        self.documentFileName = newUrl.deletingPathExtension().lastPathComponent
                        self.fileLoadLocation = newUrl.path()
                        self.updateGUI(pdfLoaded: true)
                        self.setPDFScale(to: self.VM.pdfPageScale, asNewPDF: true)
                    } else {
                        print("Could not load PDF directly from url: \(newUrl)")
                    }
                }
            }
            if let error = error {
                PRINT("File coordination error: \(error)")
            }
            
        } else {
            
            DispatchQueue.main.async {
                if let document = PDFDocument(url: url) {
                    self.pdfView.document = document
                    self.documentFileName = url.deletingPathExtension().lastPathComponent
                    self.fileLoadLocation = url.path()
                    self.updateGUI(pdfLoaded: true)
                    self.setPDFScale(to: self.VM.pdfPageScale, asNewPDF: true)
                } else {
                    PRINT("Could not load PDF from url: \(url)")
                }
            }
        }
    }

. . . .

Other relevant pList settings I've added are:

  • Supports opening documents in place - YES
  • Document types - PDFs (com.adobe.pdf)
  • UIDocumentBrowserRecentDocumentContentTypes - com.adobe.pdf
  • Application supports iTunes file sharing - YES

And iCloud is one for Entitlements with

  • iCloud Container Identifiers
  • Ubiquity Container Identifiers

. . . .

Thank you in advance!.

  • B

A few questions/comments:

-What's the error you're getting from the coordinate call?

-As a general comment, I think trying to implement file coordination "yourself" is generally a mistake. On the surface it seems like an easier option than implementing UIDocument, but in practice the file coordination process is harder to understand than it looks and using UIDocument doesn't have to mean restructuring your entire implementation around it.

-In terms of your specific implementation, you can't shift the file access out of your coordinated read as your code is doing. Any access to the file needs to be inside that file coordinated read.

Finally, I really want to push back on this point:

I can, however, copy the file to a new location in my apps directory and then open it from there and load in the data. But I really do not want to do that

Why not? APFS file cloning means that the copy itself is basically "free". If you're modifying the file, then safe-save semantics mean that you'd be writing the file to a different location, then doing an atomic replace to complete the save. Copying the file first just means you've shifted the save copy a bit. If you'll only be reading the file, then that copy may not be necessary, however, working out of your own copy makes edge cases like the file being deleted/modified/removed from the local device much easier to sort out, even if you're just going to match your behavior to the original file.

-Kevin Elliott
DTS Engineer, CoreOS/Hardware

Files App Share Context with Security scoped resource fails
 
 
Q