How to use NSOpenPanel to select a file for QLPreviewView?

341 Views Asked by At

Thanks to this answer i can see QuickLook preview of a pdf embedded in my swiftui view.

But it's only to preview the pdf that's stored in the app bundle resource. How do i use NSOpenPanel to choose a file and display it in the QLPreviewView?

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 ?? "")
    print(url)
    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 {



    var body: some View {

        // example.pdf is expected in app bundle resources
        VStack {
            MyPreview(fileName: "testing.pdf")
            Divider()
        }
        
        Button("Select PDF") {

            let openPanel = NSOpenPanel()
            openPanel.allowedFileTypes = ["pdf"]
            openPanel.allowsMultipleSelection = false
            openPanel.canChooseDirectories = false
            openPanel.canChooseFiles = true
            openPanel.runModal()
         
            
        }
    }
}

*UPDATE

This is what i've tried but nothing happens after i choose a pdf file with the openpanel. The View is blank. I think there's something that i haven't done correctly in the updateNSView.

struct ContentView: View {
@State var filename = ""

var body: some View {
    VStack {
        MyPreview(fileName: filename)
        Divider()
    }
    
    Button("Select PDF") {

        let openPanel = NSOpenPanel()
        openPanel.allowedFileTypes = ["pdf"]
        openPanel.allowsMultipleSelection = false
        openPanel.canChooseDirectories = false
        openPanel.canChooseFiles = true
        openPanel.runModal()
        print(openPanel.url!.lastPathComponent)
        filename = openPanel.url!.lastPathComponent
        //
        
    }
    }
}


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>) {
        
        let preview = QLPreviewView(frame: .zero, style: .normal)
        preview?.refreshPreviewItem()
        
    }

    typealias NSViewType = QLPreviewView

}

*UPDATE 2

My latest attempt using now a model to update the file url after choosing one with the open panel but nothing still happens.

It updates pdfurl successfully with the file url but the QLPreviewView doesn't update with the changes in updateNSView. I'm using the refreshItemPreview() which should work but i'm not sure what i'm doing wrong here

class PDFViewModel: ObservableObject {
    @Published var pdfurl = ""
}


struct MyPreview: NSViewRepresentable {
    
    @ObservedObject var pdfVM = PDFViewModel()

    func makeNSView(context: NSViewRepresentableContext<MyPreview>) -> QLPreviewView {
        let preview = QLPreviewView(frame: .zero, style: .normal)
        preview?.previewItem =  NSURL(string: pdfVM.pdfurl)
    
        return preview ?? QLPreviewView()
        
        
    }

    func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<MyPreview>)  {

        let preview = QLPreviewView(frame: .zero, style: .normal)

        preview?.refreshPreviewItem()


    }
    
    

    typealias NSViewType = QLPreviewView

}
    
    


struct ContentView: View {
    
    
    @ObservedObject var pdfVM = PDFViewModel()

    var body: some View {
        VStack {
            MyPreview()
            Divider()
        }
        
        Button("Select PDF") {
            
               let openPanel = NSOpenPanel()
                    openPanel.allowedFileTypes = ["pdf"]
                    openPanel.allowsMultipleSelection = false
                    openPanel.canChooseDirectories = false
                    openPanel.canChooseFiles = true
                    openPanel.runModal()
            
            pdfVM.pdfurl = "\(openPanel.url!)"
        
          print("the url is now \(pdfVM.pdfurl)")

            }
  
    }

}
3

There are 3 best solutions below

0
swiftnoob On BEST ANSWER

Hacky solution but it works....

I used a looped timer that monitors and sets the preview item url which is also now stored with AppStorage

class PDFViewModel: ObservableObject {
    @AppStorage("pdfurl") var pdfurl: String = ""
}


struct MyPreview: NSViewRepresentable {
    
    @ObservedObject var pdfVM = PDFViewModel()
    let preview = QLPreviewView(frame: .zero, style: .normal)

  
    func makeNSView(context: NSViewRepresentableContext<MyPreview>) -> QLPreviewView {
        preview?.autostarts = true
   
        preview?.shouldCloseWithWindow = false
        preview?.previewItem =  NSURL(string: pdfVM.pdfurl)
        updatedocument()
    
        return preview ?? QLPreviewView()
        
        
    }
    
    
    func updatedocument() {
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
     
            let delay = DispatchTime.now() + 0.3
                    DispatchQueue.main.asyncAfter(deadline: delay, execute: {
                        preview?.previewItem =  NSURL(string: pdfVM.pdfurl)
                    })
        }
    }
    
     

    func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<MyPreview>) {


       }
    
    typealias NSViewType = QLPreviewView

}
    
    


struct ContentView: View {

    @ObservedObject var pdfVM = PDFViewModel()

    var body: some View {
        VStack {
            MyPreview()
            Divider()
        }


        Button("Select Document") {
            
               let openPanel = NSOpenPanel()
                    openPanel.allowedFileTypes = ["pdf", "jpg","doc","pptx", "png", "xls", "xlsx", "docx", "jpeg", "txt"]
                    openPanel.allowsMultipleSelection = false
                    openPanel.canChooseDirectories = false
                    openPanel.canChooseFiles = true
                    openPanel.begin { (result) in
                if result == NSApplication.ModalResponse.OK {
                    if let url = openPanel.url {
                        pdfVM.pdfurl = "\(url)"
                    }
                } else if result == NSApplication.ModalResponse.cancel {

                }
                }

            }
        
        
        
  
    }


}
2
Owen Zhao On

The answer is quick direct. NSOpenPanel is subclassed from NSSavePanel. You can find want you need there.

let response = openPanel.runModal()

if response == .OK, let fileURL = openPanel.url {
    // read your file here
}

1
AudioBubble On

It looks like you are not getting the file correctly - currently, you are only getting the last part of the URL. You should get the full URL if you are not loading from Bundle.main.

This function will get you a full URL from an item:

func openPanelURL() -> URL?{
   let openPanel = NSOpenPanel()
   openPanel.allowedFileTypes = ["pdf"]
   openPanel.allowsMultipleSelection = false
   openPanel.canChooseDirectories = false
   openPanel.canChooseFiles = true
   openPanel.runModal()

   return response == .OK ? openPanel.url : nil
}

Source.

Then, you can just pass in your PreviewItemURL into the NSViewRepresentable, like so:

struct MyPreview: NSViewRepresentable {
    typealias NSViewType = QLPreviewView
    var fileName: String

    func makeNSView(context: NSViewRepresentableContext<MyPreview>) -> QLPreviewView {
        let preview = QLPreviewView(frame: .zero, style: .normal)
        preview?.autostarts = true
        preview?.previewItemURL = NSURL(string: filename)!
// WARNING! I am force unwrapping above using the !, this is unsafe for a production app
//As it can cause the app to crash. Instead, optionally unwrap using the ? and ?? operators
        return preview ?? QLPreviewView()
    }

    func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<MyPreview>) {
        
        let preview = QLPreviewView(frame: .zero, style: .normal)
        preview?.refreshPreviewItem()
        
    }
}

Docs for the NSURL init

Docs for previewItemURL

Warning: I am force unwrapping in the code above - this will crash if you have a nil or incorrect value. In a production app, I would suggest using optional unwrapping. See an article from Hacking With Swift here.