Search code examples
swiftuiappkitquicklook

How do I show QLPreviewPanel with SwiftUI on macOS?


Trying to figure out how to work with QuickLook in SwiftUI on both iOS and macOS. I suspect that in far future, there will be some unified SwiftUI QL API, but can’t see it in sight yet, so let’s work with what we have…

How do I present and configure a QLPreviewPanel from my SwiftUI view? So far, I have this:

struct ItemView: View {
    let previewPanelThing = PreviewPanelThing()
    var body: some View {
        Button("OSX preview") {
            print("osx preview")
            if let previewPanel = QLPreviewPanel.shared() {
                self.previewPanelThing.updateControllerForPanel(previewPanel)
                previewPanel.makeKeyAndOrderFront(self.previewPanelThing)
            }                   
        }
    }
}

class PreviewPanelThing: QLPreviewPanelDataSource {

    func updateControllerForPanel(_ panel: QLPreviewPanel) {
        print("updating controller")
        panel.updateController()
    }

    func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
        print("number of items")
        return 1
    }

    func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! {
        print("requesting preview item")
        let fileURL: URL = Bundle.main.url(forResource: "Thinking-of-getting-a-cat", withExtension: "png")!
        return fileURL as QLPreviewItem
    }   
}

This isn’t working. I suspect this is because the QLPreviewPanel documentation says: The preview panel follows the responder chain and adapts to the first responder willing to control it. My previewPanelThing instance isn’t in the UI and responder chain. I’m not sure how the responder chain works in SwiftUI and how to best go about it.


Solution

  • Here is possible approach on using QLPreviewView directly to preview PDF files (in this demo stored in main application bundle, but this does not change the common idea)

    Update: added variant with QLPreviewPanel on button click

    import SwiftUI
    import AppKit
    import Quartz
    
    func loadPreviewItem(with name: String) -> NSURL {
    
        let file = name.components(separatedBy: ".")
        let path = Bundle.main.path(forResource: file.first!, ofType: file.last!)
        let url = NSURL(fileURLWithPath: path!)
    
        return url
    }
    
    struct MyPreview: NSViewRepresentable {
        var fileName: String
    
        func makeNSView(context: NSViewRepresentableContext<MyPreview>) -> QLPreviewView {
            let preview = QLPreviewView(frame: .zero, style: .normal)
            preview?.autostarts = true
            preview?.previewItem = loadPreviewItem(with: fileName) as QLPreviewItem
            return preview ?? QLPreviewView()
        }
    
        func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<MyPreview>) {
        }
    
        typealias NSViewType = QLPreviewView
    
    }
    
    struct ContentView: View {
        let qlCoordinator = QLCoordinator()
    
        var body: some View {
    
            // example.pdf is expected in app bundle resources
            VStack {
                MyPreview(fileName: "example.pdf")
                Divider()
                Button("Show panel") {
                    let panel = QLPreviewPanel.shared()
                    panel?.center()
                    panel?.dataSource = self.qlCoordinator
                    panel?.makeKeyAndOrderFront(nil)
                }
            }
        }
    
        class QLCoordinator: NSObject, QLPreviewPanelDataSource {
            func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! {
                return loadPreviewItem(with: "example.pdf") as QLPreviewItem
            }
    
            func numberOfPreviewItems(in controller: QLPreviewPanel) -> Int {
                return 1
            }
        }
    }