1

I am using QLPreviewController to display images in my SwiftUI app. However, I've noticed that the swipe-to-dismiss that comes built-in when presenting QLPreviewController modally in UIKit doesn't work when presenting it using .fullScreenCover in SwiftUI. Also, when presenting modally in UIKit, it adds a UINavigationController around the preview and this doesn't happen when presented from SwiftUI as well.

To illustrate, here is code to display an image using QLPreviewController in UIKit. Please note that this is not code from my app, I am just including this to show how QLPreviewController works from UIKit. My app is 100% SwuftUI:

import UIKit
import QuickLook

class ViewController: UIViewController {
    let button = UIButton(type: .system)
    
    let url = Bundle.main.url(forResource: "foo", withExtension: "jpg")!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        button.setTitle("Preview", for: .normal)
        self.view.addSubview(button)
        button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        button.frame = CGRect(x: (view.frame.size.width - 150)/2, y: (view.frame.size.height - 44)/2, width: 150, height: 44)
    }

    @IBAction func buttonTapped(_ button: UIButton) {
        let previewController = QLPreviewController()
        previewController.dataSource = self
        present(previewController, animated: true)
    }
}

extension ViewController : QLPreviewControllerDataSource {

    @objc func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
        return 1
    }

    func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
        return url as QLPreviewItem
    }
}

And here is code doing the same thing in SwiftUI using .fullScreenCover:

import Foundation
import SwiftUI
import QuickLook

struct ContentViewSimple: View {
    
    @StateObject private var previewURL = URLContainer()
    
    var body: some View {
        VStack {
            Button("Preview", action: {
                previewURL.url = Bundle.main.url(forResource: "foo", withExtension: "jpg")
            })
        }
        .fullScreenCover(item: $previewURL.url, content: { url in
            QuickLookPreviewSimple(url: url)
        })
    }
}

class URLContainer : ObservableObject {
    @Published var url : URL?
}

struct QuickLookPreviewSimple: UIViewControllerRepresentable {
        
    let url: URL
    
    func makeUIViewController(context: Context) -> QLPreviewController {
        let controller = QLPreviewController()
        controller.dataSource = context.coordinator
        return controller
    }
    
    func updateUIViewController(_ uiViewController: QLPreviewController, context: Context) {
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self)
    }
    
    class Coordinator : NSObject, QLPreviewControllerDataSource {
        let parent: QuickLookPreviewSimple
        
        init(parent: QuickLookPreviewSimple) {
            self.parent = parent
        }
        
        @objc func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
            return 1
        }
        
        func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
            return parent.url as QLPreviewItem
        }
    }
}

As stated previously, the UIKit code presents the image wrapped in a UINavigationController and when you swipe down on the image, the view controller is dismissed. In the SwiftUI code, there is no UINavigationController and swiping down does nothing.

Some things I have tried:

  1. I can wrap the QLPreviewController in a UINavigationController in my SwiftUI code, and that helps to add the toolbar and toolbar buttons and allows me to add a Done button to dismiss, but the swipe down still doesn't work.
  2. I can use .sheet instead of .fullScreenCover, and you can swipe on the navigation bar or toolbar to dismiss, but still swiping down on the image doesn't do anything.
  3. I can use .quickLookPreview in the SwiftUI code instead of .fullScreenCover and that is an easier way to display my image in QLPreviewController and it does add the UINavigationBar, but swiping down also doesn't work.

So is this just an oversight and yet another thing broken in SwiftUI that Apple needs to fix, or is there a way to work around this limitation to allow swipe to dismiss to work on QLPreviewController in SwiftUI? Thanks.

Greg G
  • 461
  • 4
  • 14

1 Answers1

3

After working on this for a while, I have come up with an acceptable workaround until this is fixed in SwiftUI (I filed feedback with Apple on it).

Instead of using QLPreviewController in a UIViewControllerRepresentable, I am creating a new UIViewController UIViewControllerRepresentable that wraps a QLPreviewController using UIKit present() and dismisses itself when the QLPreviewController is dismissed.

Here is the updated sample code:

struct QuickLookPreviewRep: UIViewControllerRepresentable {
  
  let url: URL
  
  func makeUIViewController(context: Context) -> some UIViewController {
    return QuickLookWrapper(url: url)
  }
  
  func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
  }
}

class QuickLookWrapper : UIViewController, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
  
  var url: URL?
  var qlController : QLPreviewController?
  
  required init?(coder: NSCoder) {
    fatalError("no supported")
  }
  
  init(url: URL) {
    self.url = url
    super.init(nibName: nil, bundle: nil)
  }
  
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    if self.qlController == nil {
      self.qlController = QLPreviewController()
      self.qlController?.dataSource = self
      self.qlController?.delegate = self
      self.present(self.qlController!, animated: true)
    }
  }
  
  @objc func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
    return 1
  }
  
  func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
    return url! as any QLPreviewItem
  }
  
  func previewControllerWillDismiss(_ controller: QLPreviewController) {
    dismiss(animated: true)
  }
}
Greg G
  • 461
  • 4
  • 14