1

I've added a UIViewControllerRepresentable for UIKit's QLPreviewController which I've found in a related question:

struct QuickLookView: UIViewControllerRepresentable {
    
    var url: URL
    var onDismiss: (() -> Void) = { }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func updateUIViewController(_ viewController: UINavigationController, context: UIViewControllerRepresentableContext<Self>) {
        (viewController.topViewController as? QLPreviewController)?.reloadData()
    }
    
    func makeUIViewController(context: Context) -> UINavigationController {
        let controller = QLPreviewController()
        
        controller.dataSource = context.coordinator
        controller.reloadData()
        return UINavigationController(rootViewController: controller)
    }
    
    class Coordinator: NSObject, QLPreviewControllerDataSource {
        var parent: QuickLookView
        
        init(_ qlPreviewController: QuickLookView) {
            self.parent = qlPreviewController
            super.init()
        }
        
        func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
             1
        }
        
        func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
            self.parent.url as QLPreviewItem
        }
    }
}

In my app, I download a file (jpg/png/pdf) via Alamofire:

let destination: DownloadRequest.Destination = { _, _ in
    let documentsURL = FileManager.default.documentsDirectory
        .appendingPathComponent(document.id.string)
        .appendingPathComponent(document.name ?? "file.jpg")
    return (documentsURL, [.removePreviousFile, .createIntermediateDirectories])
}
AF
    .download(url, to: destination)
    .responseURL { (response) in
        guard let url = response.fileURL else { return }
        self.fileURL = url
        self.isShowingDoc = true
    }

...and pass its local url to the QuickLookView to present it:

@State private var isShowingDoc = false
@State private var fileURL: URL?

var body: some View {
    // ...
    .sheet(isPresented: $isShowingDoc, onDismiss: { isShowingDoc = false }) {
        QuickLookView(url: fileURL!) {
            isShowingDoc = false
        }
    }
}

What happens is that the QuickLookView opens as sheet, the file flashes (is displayed for like 0.1 seconds) and then the view goes blank:

I checked the Documents folder of the app in Finder and the file is there and matches the url passed to the QuickLookView. I've noticed that when the view is open, and I then delete the file from the folder via Finder, then the view will throw an error saying there's no such file – that means it did read it properly before it was deleted.

Note: I read somewhere that the QL controller has had issues when placed inside a navigation controller. In my view hierarchy, my views are embedded inside a NavigationView – might that cause issues?

How do I solve this?

LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174

2 Answers2

3

You just need to update the view before presenting the sheet otherwise it wont work. It can be the button title, opacity or anything. Although it looks like a hack it works fine. I will be very glad if someone explains why it happens and if there is a proper way to make it work without updating the view.


import SwiftUI

struct ContentView: View {
    @State private var fileURL: URL!
    @State private var isDisabled = false
    @State private var isDownloadFinished = false
    @State private var buttonTitle: String = "Download PDF"
    private let url = URL(string: "https://www.dropbox.com/s/bxrhk6194lf0n73/macpro_mid2010-macpro_mid2012.pdf?dl=1")!
    var body: some View {
        Button(buttonTitle) {
            isDisabled = true
            buttonTitle = "Downloading..."
            URLSession.shared.downloadTask(with: url) { location, response, error in
                guard
                    let location = location, error == nil,
                    let suggestedFilename = (response as? HTTPURLResponse)?.suggestedFilename,
                    let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
                else { return }
                fileURL = documentDirectory.appendingPathComponent(suggestedFilename)
                if !FileManager.default.fileExists(atPath: fileURL.path) {
                    do {
                        try FileManager.default.moveItem(at: location, to: fileURL)
                    } catch {
                        print(error)
                    }
                }
                DispatchQueue.main.async {
                    isDownloadFinished = true
                    buttonTitle = ""  // you need to change the view prefore presenting the sheet otherwise it wont work
                }
            }.resume()
        }
        .disabled(isDisabled == true)
        .sheet(isPresented: $isDownloadFinished) {
            isDisabled = false
            isDownloadFinished = false
            fileURL = nil
            buttonTitle = "Download PDF"
        } content: {
            if isDownloadFinished {
                PreviewController(previewItems: [PreviewItem(url: fileURL, title: fileURL?.lastPathComponent)], index: 0)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


import SwiftUI
import QuickLook

struct PreviewController: UIViewControllerRepresentable {
    
    var previewItems: [PreviewItem] = []
    
    var index: Int
    
    func makeCoordinator() -> Coordinator { .init(self) }
    
    func updateUIViewController(_ viewController: UINavigationController, context: UIViewControllerRepresentableContext<Self>) {
        (viewController.topViewController as? QLPreviewController)?.reloadData()
    }
    
    func makeUIViewController(context: Context) -> UINavigationController {
        let controller = QLPreviewController()
        controller.dataSource = context.coordinator
        controller.delegate = context.coordinator
        controller.reloadData()
        return .init(rootViewController: controller)
    }
    class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
        let previewController: PreviewController
        init(_ previewController: PreviewController) {
            self.previewController = previewController
        }
        func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
            previewController.previewItems.count
        }
        func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
            previewController.previewItems[index]
        }
    }
}

class PreviewItem: NSObject, QLPreviewItem {
    var previewItemURL: URL?
    var previewItemTitle: String?
    init(url: URL? = nil, title: String? = nil) {
        previewItemURL = url
        previewItemTitle = title
    }
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
-1

I finally got it to work – big thanks to Leo Dabus for his help in the comments.
Here's my currently working code:

@State private var isShowingDoc = false
@State private var isLoadingFile = false
@State private var fileURL: URL?

var body: some View {
    Button {
        let destination: DownloadRequest.Destination = { _, _ in
            let documentsURL = FileManager.default.documentsDirectory
                .appendingPathComponent(document.id.string)
                .appendingPathComponent(document.name ?? "file.jpg")
            return (documentsURL, [.removePreviousFile, .createIntermediateDirectories])
        }
        isLoadingFile = true
        AF
            .download(url, to: destination)
            .responseURL { (response) in
                self.isLoadingFile = false
                guard let url = response.fileURL else { return }
                isShowingDoc = true
                self.fileURL = url
            }
    } label: {
        VStack {
            Text("download")
            if isLoadingFile {
                ActivityIndicator(style: .medium)
            }
        }
    }
    .sheet(isPresented: $isShowingDoc, onDismiss: { isShowingDoc = false }) {
        QuickLookView(url: fileURL!)
    }
}

with this QuickLookView: (mostly unchanged)

struct QuickLookView: UIViewControllerRepresentable {
    
    var url: URL
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func updateUIViewController(_ viewController: UINavigationController, context: UIViewControllerRepresentableContext<Self>) {
        (viewController.topViewController as? QLPreviewController)?.reloadData()
    }
    
    func makeUIViewController(context: Context) -> UINavigationController {
        let controller = QLPreviewController()
        
        controller.dataSource = context.coordinator
        controller.reloadData()
        return UINavigationController(rootViewController: controller)
    }
    
    class Coordinator: NSObject, QLPreviewControllerDataSource {
        var parent: QuickLookView
        
        init(_ qlPreviewController: QuickLookView) {
            self.parent = qlPreviewController
            super.init()
        }
        
        func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
             1
        }
        
        func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
            self.parent.url as QLPreviewItem
        }
    }
}

As you can see, there's hardly any difference to my code from when I asked the question. Yesterday night, the fileURL was always nil for an unclear reason; yet, now it started working just fine. In exchange, the remote images in my list (not shown here) stopped working even though I haven't touched them, haha.

I don't know what's going on and what I even changed to make it work, but it works and I won't complain!

LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174