Simon
Simon

Reputation: 374

Presenting UIDocumentInteractionController with UIViewControllerRepresentable in SwiftUI

I'm creating a new iOS app using SwiftUI where ever possible. However, I want to be able to generate a PDF with some data. In a similar project without swiftUI I can do this

let docController = UIDocumentInteractionController.init(url: "PATH_TO_FILE")
                        docController.delegate = self
                        self.dismiss(animated: false, completion: {
                            docController.presentPreview(animated: true)
                        })

and as long as somewhere else in the view controller I have this:

func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
        return self
    }

I'm good to go. What I can't work out is how to apply this to a UIViewControllerRepresentable and have it working in SwiftUI. Should my UIViewControllerRepresentable be aiming to be a UIViewController? How do I then set the delegate and presentPreview? Will this overlay any view and display full screen over my SwiftUI app as it does for my standard iOS app? Thanks

Upvotes: 9

Views: 6361

Answers (4)

user2770532
user2770532

Reputation: 21

I had the same issue with the accepted answer - the view would show twice. All I had to do is to to reset self.isActive:

        if self.isActive.wrappedValue && docController.delegate == nil { // to not show twice
            self.isActive.wrappedValue = false // add this!

Upvotes: -1

Enrique
Enrique

Reputation: 1623

Using QLPreviewController

I know the question is about UIDocumentInteractionController, but if you want to present a PDF file (for example), you can use a QLPreviewController.

Local file

Presenting a local file:

Show Local File

import SwiftUI

struct DocView: View {
    @State private var buttonPressed: Bool = false

    var body: some View {
        Button {
            buttonPressed = true
        } label: {
            Text("Show PDF file")
        }
        .sheet(isPresented: $buttonPressed) {
            let localURL = Bundle.main.url(forResource: "Example", withExtension: "pdf")!
            PreviewController(url: localURL)
        }
    }
}

Remote file

Please see this gist if you need to present a remote file.

Show Remote File

PreviewController

The UIViewControllerRepresentable for QLPreviewController.

import QuickLook
import SwiftUI

struct PreviewController: UIViewControllerRepresentable {
    @Environment(\.dismiss) private var dismiss

    let url: URL

    func makeUIViewController(context: Context) -> UINavigationController {
        let controller = QLPreviewController()
        controller.dataSource = context.coordinator
        controller.navigationItem.leftBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .done, target: context.coordinator,
            action: #selector(context.coordinator.dismiss)
        )

        let navigationController = UINavigationController(rootViewController: controller)
        return navigationController
    }

    func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}

    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self)
    }

    class Coordinator: QLPreviewControllerDataSource {
        let parent: PreviewController

        init(parent: PreviewController) {
            self.parent = parent
        }

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

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

        @objc func dismiss() {
            parent.dismiss()
        }
    }
}

Upvotes: 3

Asperi
Asperi

Reputation: 258117

Here is possible approach to integrate UIDocumentInteractionController for usage from SwiftUI view.

demo

Full-module code. Tested with Xcode 11.2 / iOS 13.2

import SwiftUI
import UIKit

struct DocumentPreview: UIViewControllerRepresentable {
    private var isActive: Binding<Bool>
    private let viewController = UIViewController()
    private let docController: UIDocumentInteractionController

    init(_ isActive: Binding<Bool>, url: URL) {
        self.isActive = isActive
        self.docController = UIDocumentInteractionController(url: url)
    }
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPreview>) -> UIViewController {
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<DocumentPreview>) {
        if self.isActive.wrappedValue && docController.delegate == nil { // to not show twice
            docController.delegate = context.coordinator
            self.docController.presentPreview(animated: true)
        }
    }
    
    func makeCoordinator() -> Coordintor {
        return Coordintor(owner: self)
    }
    
    final class Coordintor: NSObject, UIDocumentInteractionControllerDelegate { // works as delegate
        let owner: DocumentPreview
        init(owner: DocumentPreview) {
            self.owner = owner
        }
        func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
            return owner.viewController
        }
        
        func documentInteractionControllerDidEndPreview(_ controller: UIDocumentInteractionController) {
            controller.delegate = nil // done, so unlink self
            owner.isActive.wrappedValue = false // notify external about done
        }
    }
}

// Demo of possible usage
struct DemoPDFPreview: View {
    @State private var showPreview = false // state activating preview

    var body: some View {
        VStack {
            Button("Show Preview") { self.showPreview = true }
                .background(DocumentPreview($showPreview, // no matter where it is, because no content
                            url: Bundle.main.url(forResource: "example", withExtension: "pdf")!))
        }
    }
}

struct DemoPDFPreview_Previews: PreviewProvider {
    static var previews: some View {
        DemoPDFPreview()
    }
}

Upvotes: 11

Theo Lampert
Theo Lampert

Reputation: 1276

I ended up doing something like the following as I wasn't able to get this working reliably with UIViewControllerRepresentable and the above answer. You might need to edit / extend this for your usecase.

class DocumentController: NSObject, ObservableObject, UIDocumentInteractionControllerDelegate {
    let controller = UIDocumentInteractionController()
    func presentDocument(url: URL) {
        controller.delegate = self
        controller.url = url
        controller.presentPreview(animated: true)
    }

    func documentInteractionControllerViewControllerForPreview(_: UIDocumentInteractionController) -> UIViewController {
        return UIApplication.shared.windows.first!.rootViewController!
    }
}

Usage:

struct DocumentView: View {
  @StateObject var documentController = DocumentController()

  var body: some View {
      Button(action: {
          documentController.presentDocument(url: ...)
      }, label: {
          Text("Show Doc")
      })
  }
}

Upvotes: 7

Related Questions