'UIDocumentPickerViewController doesn't show any contents on Mac Catalyst

UIDocumentPickerViewController works on iOS but not on Mac Catalyst. Is there any alternatives to workaround this issue? BTW, NSOpenPanel is unavailable on Mac Catalyst.

enter image description here



Solution 1:[1]

There's extra code in @UnchartedWorks' excellent answer. Here's a cleaner version with some options, more copy/paste-able into your code. This works on iOS, iPadOS, and Mac Catalyst (without using a #if conditional).

import Foundation
import SwiftUI
import MobileCoreServices

/// A wrapper for a UIDocumentPickerViewController that acts as a delegate and passes the selected file to a callback
///
/// DocumentPicker also sets allowsMultipleSelection to `false`.
final class DocumentPicker: NSObject {

    /// The types of documents to show in the picker
    let types: [String]

    /// The callback to call with the selected document URLs
    let callback: ([URL]) -> ()

    /// Should the user be allowed to select more than one item?
    let allowsMultipleSelection: Bool

    /// Creates a DocumentPicker, defaulting to selecting folders and allowing only one selection
    init(for types: [String] = [String(kUTTypeFolder)],
         allowsMultipleSelection: Bool = false,
         _ callback: @escaping ([URL]) -> () = { _ in }) {
        self.types = types
        self.allowsMultipleSelection = allowsMultipleSelection
        self.callback = callback
    }

    /// Returns the view controller that must be presented to display the picker
    lazy var viewController: UIDocumentPickerViewController = {
        let vc = UIDocumentPickerViewController(documentTypes: types, in: .open)
        vc.delegate = self
        vc.allowsMultipleSelection = self.allowsMultipleSelection
        return vc
    }()

}

extension DocumentPicker: UIDocumentPickerDelegate {
    /// Delegate method that's called when the user selects one or more documents or folders
    ///
    /// This method calls the provided callback with the URLs of the selected documents or folders.
    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
        callback(urls)
    }

    /// Delegate method that's called when the user cancels or otherwise dismisses the picker
    ///
    /// Does nothing but close the picker.
    func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
        controller.dismiss(animated: true, completion: nil)
        print("cancelled")
    }
}

IMPORTANT: Add the "com.apple.security.files.user-selected.read-write" (Boolean, set to YES) entitlement to your app's entitlements file or it will crash when you open the picker on the Mac. If you only need read access, you can use "com.apple.security.files.user-selected.read" instead.

Sample usage:

struct ContentView: View {
    /// The view controller for the sheet that lets the user select the project support folder
    ///
    /// Yes, I said "view controller" - we need to go old school for Catalyst and present a view controller from the root view controller manually.
    @State var filePicker: DocumentPicker

    init() {
        // Setting filePicker like this lets us keep DocumentPicker in the view
        // layer (instead of a model or view model), lets SwiftUI hang onto
        // the reference to it, and lets you specify another function to
        // call (e.g. one from a view model) using info passed into your
        // init method.
        _filePicker = State(initialValue: DocumentPicker({urls in
                print(urls)
            }))
    }

    var body: some View {
        Button("Pick a folder") {
            self.presentDocumentPicker()
        }
    }

    /// Presents the document picker from the root view controller
    ///
    /// This is required on Catalyst but works on iOS and iPadOS too, so we do it this way instead of in a UIViewControllerRepresentable
    func presentDocumentPicker() {
        let viewController = UIApplication.shared.windows[0].rootViewController!
        let controller = self.filePicker.viewController
        viewController.present(controller, animated: true)
    }

}

Solution 2:[2]

screenshot The following example is for Mac Catalyst. If you want to support UIDocumentPickerViewController on iOS and Mac Catalyst, you should use

#if targetEnvironment(macCatalyst)
//code for Mac Catalyst
#endif

How to support UIDocumentPickerViewController on Mac Catalyst

//SceneDelegate.swift
import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    var picker = DocumentPicker()

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    let contentView = ContentView()
        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()

            window.rootViewController?.present(picker.viewController, animated: true)
        }
    }
}


//ContentView.swift
final class DocumentPicker: NSObject, UIViewControllerRepresentable {
    typealias UIViewControllerType = UIDocumentPickerViewController

    lazy var viewController: UIDocumentPickerViewController = {
        let vc = UIDocumentPickerViewController(documentTypes: ["public.data"], in: .open)
        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, completion: nil)
        print("cancelled")
    }
}

Thanks for Simon, without his help, I can't solve this issue.

Solution 3:[3]

After many attempts, I managed to find the code below that works well with Xcode 11.3.1 on Mac Catalyst.

import SwiftUI

    final class DocumentPicker: NSObject, UIViewControllerRepresentable, ObservableObject {
        typealias UIViewControllerType = UIDocumentPickerViewController
        @Published var urlsPicked = [URL]()

        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]) {
            urlsPicked = urls
            print("DocumentPicker geoFolder.geoFolderPath: \(urlsPicked[0].path)")
        }

    //    func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
    //        controller.dismiss(animated: true) {
    //        }
    //    }
    }

and I use the code above, for example with:

import SwiftUI

    struct ContentView: View {
        @ObservedObject var picker = DocumentPicker()
        @State private var urlPick = ""

        var body: some View {
                HStack {
                    urlPickedFoRTextField()
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                    Spacer()
                    Button(action: {
                        #if targetEnvironment(macCatalyst)
                        let viewController = UIApplication.shared.windows[0].rootViewController!
                        viewController.present(self.picker.viewController, animated: true)
                        self.picker.objectWillChange.send()
                        #endif
                        print("Hai premuto il pulsante per determinare il path della GeoFolder")
                    }) {
                        Image(systemName: "square.and.arrow.up")
                    }
                }
                .padding()
        }

        private func urlPickedFoRTextField() -> some View {
            if picker.urlsPicked.count > 0 {
                DispatchQueue.main.async {
                    self.urlPick = self.picker.urlsPicked[0].path
                }
            }
            return TextField("", text: $urlPick)
        }
    }

I hope I was helpful.

Solution 4:[4]

You have to go to your Target -> Signing & Capabilities and set the File access.

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
Solution 2 webcpu
Solution 3 Cesare Piersigilli
Solution 4 adek_dev