Use of QLPreviewPanel in a sandboxed app

I'm trying to use QLPreviewPanel with a file stored in Caches, but my app crashes when sandbox is enabled. I've tried different directories instead of Caches but they don't seem to make any difference.


Here is a bit more background about what I'm doing: The app is receiving assets in a packed form from the Internet. The assets can be images, text files, documents, etc. After unpacking, each asset is contained in a Data. What I would like to do is present a QuickLook view of this asset to the user. So I don't even need to use the filesystem, I could do it directly from memory if QuickLook supported that.


I originally posted this question in a reply to: https://forums.developer.apple.com/message/255625. I'm reposting as a new question in the hope of getting more visibility. Any advice is appreciated.

Replies

Would be better to postactual code and detail of the crash (whee, which message...)

Sure, just paste this code to a new project and connect the IBAction to a button.


class ViewController: NSViewController, QLPreviewPanelDataSource {
    var previewPanel: QLPreviewPanel!
    var url: URL!

    override func viewDidAppear() {
        let path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
        self.url = URL(fileURLWithPath: path).appendingPathComponent("test.html")
        let data = "<html><body><h1>Hello</h1></body></html>".data(using: .utf8)!
        try! data.write(to: url)
        NSApp.mainWindow!.nextResponder = self
    }

    @IBAction func toggleQuickLook(_ send: NSButton) {
        if QLPreviewPanel.sharedPreviewPanelExists() && QLPreviewPanel.shared()!.isVisible {
            QLPreviewPanel.shared()!.orderOut(nil)
        } else {
            QLPreviewPanel.shared()!.makeKeyAndOrderFront(nil)
        }
    }

    override func acceptsPreviewPanelControl(_ panel: QLPreviewPanel!) -> Bool {
        return true
    }

    override func beginPreviewPanelControl(_ panel: QLPreviewPanel!) {
        panel.dataSource = self
        self.previewPanel = panel
    }

    override func endPreviewPanelControl(_ panel: QLPreviewPanel!) {
        self.previewPanel = nil
    }

    func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
        return 1
    }

    func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! {
        return self.url as QLPreviewItem
    }
}


The error is: Thread 1: EXC_BAD_ACCESS (code=1, address=0x20)

Disclaimer: I am not a Swifty and it may be that what I am about to say is irrelevant.


In this code:


let path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]

self.url = URL(fileURLWithPath: path).appendingPathComponent("test.html")


If it was objective C, I see a problem and a crash. The NSSearchPathForDirectoriesInDomains returns an array of strings and then you would need to get one of the NSString objects out of the array. Like I said, I am not a swifty so maybe this code is pulling a string object out of the array.

No, it is not crashing there, the URL is set correctly. The crash happens after toggleQuickLook is called and only when sandbox is enabled.

>>my app crashes when sandbox is enabled


So, if it's not crashing without the sandbox, then the probability is that you're trying to access a directory that requires explicit user approval to access.


>>I've tried different directories instead of Caches but they don't seem to make any difference


Did you try the Application Support directory? That's one that can accessed while sandboxed. (Note: When using Application Support unsandboxed, the convention is to create your own subfolder named with your app's bundle ID, so you don't clobber other app's files. When sandboxed, you don't need to create the subfolder — though of course you can if you want.)


The problem you have is that line 06 of your code fragment isn't safe, and NSSearchPathForDirectoriesInDomains returns no useful error information if it fails. (You should regard this function as outdated.) Instead, use FileManager.url(for:in:appropriateFor:create:) instead. It throws an error if something goes wrong.


If you do use a function that returns an array (FileManager.urls(for:in:) is the modern equivalent of NSSearchPathForDirectoriesInDomains), don't use the array subscript without first check that the array has something in it. I suspect this is the actual cause of your crash right now.


Or, you may actually want to use a temp directory for this. Instructions for creating a temp directory in a modern way are here:


https://developer.apple.com/documentation/foundation/filemanager/1407693-url


under the "Discussion" heading. (In this case, the 3rd parameter is used only to determine which disk volume to use for the temp directory. IIRC you can pass nil if you don't care.)


In general, your code shows a lot of places where you are "ignoring" optionality (using the "!" operator to prevent messages from the compiler counts as ignoring). Your code will be a lot more robust if you check for unexpected nil results at the point where they're generated, and don't propagate values with optional types through your code. The "guard let" and "if let" constructs are your best friend here. 🙂

Changing lines 3-4 to the following doesn't make any difference:


self.url = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("test.html")


In any case the file is created succesfully, so I definitely have the right access permissions. I'm assuming that QuickLook is running under different permissions that is causing the app to crash. So is there a location that my app can write to and QuickLook can read when running sandboxed? What should this line be in order for it to work?


PS: I'm well aware of the dangers of force unwrapping/force trying, this is a reduced example...

Thanks for the clarification. Can I ask for a bit more clarification?


Are you trying to write an app that has a QuickLook generator? Or are you just trying to use the QLPreviewPanel referenced in that other thread?


Here is my concern. That other thread was from 2017. I had never heard of those classes before. There is no guaraneed that they ever worked in the sandbox or that they will in the future.


Why do you want the QuickLook view anyway? How is that better than just displaying the data in a window? Quicklook has changed since 2017. The QLPreviewPanel may not work anymore. Have you tried the QLPreviewView instead? You might not get some of the toolbar features in a modern QuickLook window, but you could easly reproduce that in your own window.

Hi, I did try QLPreviewView too and seems to have exacly the same issue.

Turns out this works if I enable com.apple.security.network.client entitlement. My app doesn't do any outgoing network connections, are there any workarounds to use QuickLook without this entitlement?

After one year from this post this is the only solution I found, but also my app doesn't use network connection so I would remove this entitlement.

Any news?