dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

Mobile Support For Native File Selection

Open mcmah309 opened this issue 9 months ago • 3 comments

Feature Request

rsx! {
    input { r#type: "file" }
}

The above does not work on mobile. This should be supported.

More generalized version of: https://github.com/DioxusLabs/dioxus/issues/3720

Prior Art:

  • https://github.com/PolyMeilex/rfd
  • https://github.com/tauri-apps/tauri/issues/6517#issuecomment-1488052304
  • https://github.com/miguelpruivo/flutter_file_picker
  • https://github.com/kineapps/flutter_file_dialog

Relevant Internal Discussion:

  • https://github.com/DioxusLabs/dioxus/pull/1107

Blocked by:

  • https://github.com/DioxusLabs/dioxus/issues/3870

mcmah309 avatar Mar 09 '25 01:03 mcmah309

Workaround until this is implemented:

Cargo.toml

[target.'cfg(target_os = "ios")'.dependencies]
objc2 = "0.6.0"
objc2-foundation = { version = "0.3.0", features = ["NSString"] }
objc2-ui-kit = { version = "0.3.0", features = [
    "objc2-uniform-type-identifiers",
] }
objc2-uniform-type-identifiers = "0.3.0"
tokio = "1.44.1"
use std::cell::Cell;
use std::ops::Deref;
use std::path::PathBuf;

use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2::{DefinedClass, MainThreadOnly, define_class, msg_send};
use objc2_foundation::{MainThreadMarker, NSArray, NSObject, NSObjectProtocol, NSString, NSURL};
use objc2_ui_kit::{UIApplication, UIDocumentPickerDelegate, UIDocumentPickerViewController};
use objc2_uniform_type_identifiers::UTType;

struct DelegateIvars {
    is_done: Cell<bool>,
    was_cancelled: Cell<bool>,
    picked_paths: Cell<Vec<PathBuf>>,
}

impl DelegateIvars {
    fn new() -> Self {
        Self {
            is_done: Cell::new(false),
            was_cancelled: Cell::new(false),
            picked_paths: Cell::new(Vec::new()),
        }
    }
}

define_class!(
    // SAFETY:
    // - The superclass NSObject does not have any subclassing requirements.
    // - `Delegate` does not implement `Drop`.
    #[unsafe(super = NSObject)]
    #[thread_kind = MainThreadOnly]
    #[name = "Delegate"]
    #[ivars = DelegateIvars]
    struct Delegate;

    // SAFETY: `NSObjectProtocol` has no safety requirements.
    unsafe impl NSObjectProtocol for Delegate {}

    unsafe impl UIDocumentPickerDelegate for Delegate {
        #[unsafe(method(documentPicker:didPickDocumentsAtURLs:))]
        fn document_picker_did_pick_documents_at_urls(
            &self,
            _document_picker: &UIDocumentPickerViewController,
            urls: &NSArray<NSURL>,
        ) {
            let mut url_paths: Vec<PathBuf> = Vec::with_capacity(urls.count());
            for i in 0..urls.count() {
                let url = unsafe { urls.objectAtIndex(i).path().unwrap().to_string() };
                url_paths.push(PathBuf::from(url));
            }

            self.ivars().picked_paths.set(url_paths);
            self.ivars().is_done.set(true);
        }

        #[unsafe(method(documentPickerWasCancelled:))]
        fn document_picker_was_cancelled(&self, _document_picker: &UIDocumentPickerViewController) {
            self.ivars().was_cancelled.set(true);
            self.ivars().is_done.set(true);
        }
    }
);

impl Delegate {
    fn new(mtm: MainThreadMarker) -> Retained<Self> {
        let this = Self::alloc(mtm).set_ivars(DelegateIvars::new());
        // SAFETY: The signature of `NSObject`'s `init` method is correct.
        unsafe { msg_send![super(this), init] }
    }
}

pub async fn open_file_picker(animated: bool, document_types: &[&str]) -> Option<Vec<PathBuf>> {
    let mtm = MainThreadMarker::new().unwrap();
    let app = UIApplication::sharedApplication(mtm);

    unsafe {
        let document_types: Vec<_> = document_types
            .iter()
            .flat_map(|s| UTType::typeWithFilenameExtension(NSString::from_str(s).deref()))
            .collect();
        let document_types: Vec<_> = document_types.iter().map(|t| t.deref()).collect();
        let document_types = NSArray::from_slice(document_types.as_slice());

        let picker = UIDocumentPickerViewController::alloc(mtm);
        let picker = UIDocumentPickerViewController::initForOpeningContentTypes_asCopy(
            picker,
            &document_types,
            true,
        );

        let delegate = Delegate::new(mtm);
        picker.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
        picker.setAllowsMultipleSelection(true);
        let window = app.keyWindow().unwrap();
        let current_vc = window.rootViewController().unwrap();

        current_vc.presentViewController_animated_completion(&picker, animated, None);

        while !delegate.ivars().is_done.get() {
            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
        }

        if delegate.ivars().was_cancelled.get() {
            None
        } else {
            Some(delegate.ivars().picked_paths.take())
        }
    }
}

maun avatar Apr 01 '25 14:04 maun

Workaround until this is implemented

Looks awesome! Would you consider integrating this into the framework?

mcmah309 avatar Apr 01 '25 15:04 mcmah309

Workaround until this is implemented

Looks awesome! Would you consider integrating this into the framework?

I polished and published it here: https://crates.io/crates/apple-utils

maun avatar Apr 03 '25 11:04 maun