'How to popup a document picker in ios and macos using catalyst
I'm trying to popup a document picker using mac catalyst, but all I get is a blank screen.
All works well on ios 13.2.2 on iPad and iPhone, but not on macos 10.15.1 catalina.
Does anyone knows how to popup a document picker in both ios and macos using catalyst?
with my entitlements file having:
<key>com.apple.security.app-sandbox</key>
<false/>
Here is the test code that shows the problem.
import Foundation
import SwiftUI
struct ContentView: View {
@State var isFilePickerShown = false
var body: some View {
VStack {
Button(action: { self.isFilePickerShown.toggle() }) {
Image(systemName: "rectangle.and.paperclip").resizable().frame(width: 70, height: 70)
}
}.sheet(isPresented: $isFilePickerShown, onDismiss: {self.isFilePickerShown = false}) {
DocPickerViewController(callback: self.filePicked, onDismiss: { self.isFilePickerShown = false })
}
}
func filePicked(_ url: URL) {
print("\nThe url is: \(url)")
}
}
struct DocPickerViewController: UIViewControllerRepresentable {
private let docTypes: [String] = ["com.adobe.pdf", "public.text", "public.composite-content"]
var callback: (URL) -> ()
private let onDismiss: () -> Void
init(callback: @escaping (URL) -> (), onDismiss: @escaping () -> Void) {
self.callback = callback
self.onDismiss = onDismiss
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext<DocPickerViewController>) {
}
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let controller = UIDocumentPickerViewController(documentTypes: docTypes, in: .import)
controller.allowsMultipleSelection = false
controller.delegate = context.coordinator
return controller
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
var parent: DocPickerViewController
init(_ pickerController: DocPickerViewController) {
self.parent = pickerController
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
parent.callback(urls[0])
parent.onDismiss()
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
parent.onDismiss()
}
}
}
I got something going with the following code. But really this is a messy business and I feel this is not the answer.
Now I have the same problem trying to display a UIActivityViewController. I can display it on Mac using the same approach but I can't control where it shows up. It's always at (0,0).
struct FilePicker: UIViewControllerRepresentable {
private let docTypes: [String] = ["com.adobe.pdf", "public.text", "public.composite-content"]
private let controller: FilePickerController?
var callback: (URL) -> ()
private let onDismiss: () -> Void
init(callback: @escaping (URL) -> (), onDismiss: @escaping () -> Void) {
self.callback = callback
self.onDismiss = onDismiss
self.controller = FilePickerController(documentTypes: docTypes, mode: .import)
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func updateUIViewController(_ uiViewController: FilePickerController, context: UIViewControllerRepresentableContext<FilePicker>) { }
func makeUIViewController(context: Context) -> FilePickerController {
return controller!
}
class Coordinator: NSObject, FilePickerControllerDelegate {
var parent: FilePicker
init(_ filePicker: FilePicker) {
self.parent = filePicker
super.init()
self.parent.controller?.delegate = self
}
func documentPicker(_ controller: FilePickerController, didPickDocumentsAt urls: [URL]) {
parent.callback(urls[0])
parent.onDismiss()
}
func documentPickerWasCancelled(_ controller: FilePickerController) {
parent.onDismiss()
}
}
}
protocol FilePickerControllerDelegate: class {
func documentPickerWasCancelled(_ controller: FilePickerController)
func documentPicker(_ controller: FilePickerController,
didPickDocumentsAt urls: [URL])
}
class FilePickerController: UIViewController, UIDocumentPickerDelegate {
weak var delegate: FilePickerControllerDelegate?
let viewController: UIDocumentPickerViewController?
public init(documentTypes: [String], mode: UIDocumentPickerMode) {
viewController = UIDocumentPickerViewController(documentTypes: documentTypes, in: mode)
super.init(nibName: nil, bundle: nil)
}
required public init?(coder: NSCoder) {
viewController = UIDocumentPickerViewController(coder: coder)
super.init(coder: coder)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let viewController = viewController {
viewController.delegate = self
self.present(viewController, animated: animated)
}
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
self.dismiss(animated: false) {
self.delegate?.documentPicker(self, didPickDocumentsAt: urls)
}
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
self.dismiss(animated: false) {
self.delegate?.documentPickerWasCancelled(self)
}
}
}
I call it like this:
.sheet(isPresented: $isShown, onDismiss: {self.isShown = false}) {
#if targetEnvironment(macCatalyst)
FilePicker(callback: self.filePicked, onDismiss: { self.isShown = false })
#else
DocPickerViewController(callback: self.filePicked, onDismiss: { self.isShown = false })
#endif
}
Solution 1:[1]
For all with code from hstdt
and workingdog
.
import SwiftUI
struct ContentView: View {
@State private var isFilePickerShown = false
@State private var picker = DocumentPicker()
var body: some View {
VStack {
Button(action: {
self.isFilePickerShown.toggle()
#if targetEnvironment(macCatalyst)
UIApplication.shared.windows[0].rootViewController!.present(self.picker.viewController, animated: true)
#endif
}) {
Image(systemName: "rectangle.and.paperclip").resizable().frame(width: 70, height: 70)
}
}
.sheet(isPresented: $isFilePickerShown, onDismiss: {self.isFilePickerShown = false}) {
DocPickerViewController(callback: self.filePicked, onDismiss: { self.isFilePickerShown = false })
}
}
func filePicked(_ url: URL) {
print("\nThe url is: \(url)")
}
}
and now create new swiftui file DocumentPicker
for MacOS:
import SwiftUI
final class DocumentPicker: NSObject, UIViewControllerRepresentable {
typealias UIViewControllerType = UIDocumentPickerViewController
lazy var viewController:UIDocumentPickerViewController = {
// For picked only folder
let vc = UIDocumentPickerViewController(documentTypes: ["public.folder"], in: .open)
// For picked every document
// let vc = UIDocumentPickerViewController(documentTypes: ["public.data"], in: .open)
// For picked only images
// let vc = UIDocumentPickerViewController(documentTypes: ["public.image"], in: .open)
vc.allowsMultipleSelection = false
// vc.accessibilityElements = [kFolderActionCode]
// vc.shouldShowFileExtensions = true
vc.delegate = self
return vc
}()
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPicker>) -> UIDocumentPickerViewController {
viewController.delegate = self
return viewController
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext<DocumentPicker>) {
}
}
extension DocumentPicker: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
print(urls)
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
controller.dismiss(animated: true) {
}
print("cancelled")
}
}
This code popup a document picker in both iOS and MacOS using catalyst.
With MacOS Catalyst work with DocumentPicker
Class and with iOS with DocPickerViewController
write by workingdog
(see workingdog
post above)
Solution 2:[2]
Workaround: present picker controller for catalyst
UIApplication.shared.firstKeyWindow?.rootViewController!.present(self.picker.controller!, animated: true)
extension UIApplication {
public var firstKeyWindow: UIWindow? {
windows.first(where: { $0.isKeyWindow })
}
}
Solution 3:[3]
That's the example from Cesare Piersigilli on GitHub: https://github.com/AndreasPrang/Catalyst-File-Picker
Solution 4:[4]
if target macos 11+ then try to use .fileImporter presentation modifier
.fileImporter(isPresented: $showDocumentPicker,
allowedContentTypes: [.folder],
allowsMultipleSelection: true)
{ result in
// processing results Result<[URL], Error>
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Cesare Piersigilli |
Solution 2 | |
Solution 3 | Andreas Prang |
Solution 4 | Sergei Volkov |