dioxus
dioxus copied to clipboard
Mobile Support For Native File Selection
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
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())
}
}
}
Workaround until this is implemented
Looks awesome! Would you consider integrating this into the framework?
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