Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changes/add-ios-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@tauri-apps/api": patch:enhance
---

Add support for AppDocuments, AppLibrary, and AppTemp folders.
Add documentation for usage of different path methods.
Add sample page for available paths.
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ class PathPlugin(private val activity: Activity): Plugin(activity) {
resolvePath(invoke, activity.cacheDir.absolutePath)
}

@Command
fun getFilesDir(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
resolvePath(invoke, activity.filesDir.absolutePath)
} else {
resolvePath(invoke, activity.applicationInfo.dataDir)
}
}

@Command
fun getHomeDir(invoke: Invoke) {
resolvePath(invoke, Environment.getExternalStorageDirectory().absolutePath)
Expand Down
54 changes: 54 additions & 0 deletions crates/tauri/mobile/ios-api/Sources/Tauri/PathPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

import Foundation

class PathPlugin: Plugin {
private let fileManager = FileManager.default

@objc func getCacheDir(_ invoke: Invoke) {
if let url = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
resolvePath(invoke, pathValue: url.path)
} else {
resolvePath(invoke, pathValue: nil)
}
}

@objc func getDocumentsDir(_ invoke: Invoke) {
if let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
resolvePath(invoke, pathValue: url.path)
} else {
resolvePath(invoke, pathValue: nil)
}
}

@objc func getLibraryDir(_ invoke: Invoke) {
if let url = fileManager.urls(for: .libraryDirectory, in: .userDomainMask).first {
resolvePath(invoke, pathValue: url.path)
} else {
resolvePath(invoke, pathValue: nil)
}
}

@objc func getTempDir(_ invoke: Invoke) {
let tempDir = NSTemporaryDirectory()
resolvePath(invoke, pathValue: tempDir)
}

private func resolvePath(_ invoke: Invoke, pathValue: String?) {
var obj: JSObject = [:]
if let pathValue = pathValue {
obj["path"] = pathValue
} else {
obj["path"] = NSNull()
}
invoke.resolve(obj)
}
}

@_cdecl("init_path_plugin")
func initPathPlugin() -> Plugin {
return PathPlugin()
}

2 changes: 1 addition & 1 deletion crates/tauri/scripts/bundle.global.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
#[macro_export]
macro_rules! ios_plugin_binding {
($fn_name: ident) => {
tauri::swift_rs::swift!(fn $fn_name() -> *const ::std::ffi::c_void);
$crate::swift_rs::swift!(fn $fn_name() -> *const ::std::ffi::c_void);
}
}
#[cfg(target_os = "macos")]
Expand Down
15 changes: 15 additions & 0 deletions crates/tauri/src/path/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,21 @@ impl<R: Runtime> PathResolver<R> {
self.call_resolve("getCacheDir")
}

/// Returns the path to the app's documents directory.
pub fn app_documents_dir(&self) -> Result<PathBuf> {
self.call_resolve("getFilesDir")
}

/// Returns the path to the app's library directory.
pub fn app_library_dir(&self) -> Result<PathBuf> {
self.call_resolve("getFilesDir")
}

/// Returns the path to the app's temporary directory.
pub fn app_temp_dir(&self) -> Result<PathBuf> {
self.call_resolve("getCacheDir")
}

/// Returns the path to the suggested directory for your app's log files.
pub fn app_log_dir(&self) -> Result<PathBuf> {
self
Expand Down
23 changes: 23 additions & 0 deletions crates/tauri/src/path/desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,29 @@ impl<R: Runtime> PathResolver<R> {
.map(|dir| dir.join(&self.0.config().identifier))
}

/// Returns the path to the app's documents directory.
///
/// Not supported on desktop, returns error.
pub fn app_documents_dir(&self) -> Result<PathBuf> {
Err(Error::UnknownPath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't we want these methods to resolve (or fall back) on desktop? Otherwise we'd need to handle desktop/mobile paths separately in the application.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what a sensible fallback would be for these on Desktop implementations would be, since these may not have direct translations to MacOS and Windows apps.
Do you have any thoughts on what a reasonable fallback would be for these methods on desktop?

}

/// Returns the path to the app's library directory.
///
/// Not supported on desktop, returns error.
pub fn app_library_dir(&self) -> Result<PathBuf> {
Err(Error::UnknownPath)
}

/// Returns the path to the app's temporary directory.
///
/// Not supported on desktop, returns error.
pub fn app_temp_dir(&self) -> Result<PathBuf> {
Err(Error::UnknownPath)
}
}

impl<R: Runtime> PathResolver<R> {
/// Returns the path to the suggested directory for your app's log files.
///
/// ## Platform-specific
Expand Down
257 changes: 257 additions & 0 deletions crates/tauri/src/path/ios.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use super::{Error, Result};
use crate::{plugin::PluginHandle, AppHandle, Runtime};
use std::path::{Path, PathBuf};

#[cfg(target_os = "ios")]
use super::desktop;

/// A helper class to access the mobile path APIs.
///
/// ## Hybrid Approach
///
/// This resolver uses a hybrid approach that combines both desktop and iOS-specific implementations
/// to maintain backwards compatibility while providing native iOS path resolution for mobile-specific
/// directories.
///
/// ### Why This Approach is Necessary
///
/// Prior to the introduction of mobile-specific path methods (`appDocumentsDir`, `appLibraryDir`,
/// `appTempDir`, `appCacheDir`), iOS applications used the desktop path resolution implementation,
/// which relied on the `dirs` crate and other desktop-oriented path resolution mechanisms. These
/// paths worked correctly on iOS Simulator but may not have matched the exact native iOS sandbox
/// paths on real devices.
///
/// To maintain backwards compatibility for existing code that uses methods like `documentDir()`,
/// `cacheDir()`, `configDir()`, etc., we continue to delegate these methods to the desktop
/// implementation. This ensures that existing applications continue to work without modification.
///
/// ### How It Works
///
/// The resolver contains both an `AppHandle<R>` (for desktop method delegation) and a
/// `PluginHandle<R>` (for iOS native plugin calls). The routing logic is as follows:
///
/// - **Most methods** (e.g., `audio_dir()`, `cache_dir()`, `config_dir()`, `document_dir()`, etc.):
/// Always delegate to the desktop implementation via `desktop::PathResolver`. This maintains
/// backwards compatibility with existing code.
///
/// - **Four mobile-specific methods** (`app_cache_dir()`, `app_documents_dir()`, `app_library_dir()`,
/// `app_temp_dir()`): Use the iOS Swift plugin to call native iOS APIs:
/// - `app_cache_dir()` → `getCacheDir()` → `FileManager.default.urls(for: .cachesDirectory, ...)`
/// - `app_documents_dir()` → `getDocumentsDir()` → `FileManager.default.urls(for: .documentDirectory, ...)`
/// - `app_library_dir()` → `getLibraryDir()` → `FileManager.default.urls(for: .libraryDirectory, ...)`
/// - `app_temp_dir()` → `getTempDir()` → `NSTemporaryDirectory()`
pub struct PathResolver<R: Runtime> {
pub(crate) app: AppHandle<R>,
pub(crate) plugin: PluginHandle<R>,
}

impl<R: Runtime> Clone for PathResolver<R> {
fn clone(&self) -> Self {
Self {
app: self.app.clone(),
plugin: self.plugin.clone(),
}
}
}

#[derive(serde::Deserialize)]
struct PathResponse {
path: Option<String>,
}

impl<R: Runtime> PathResolver<R> {
/// Returns the final component of the `Path`, if there is one.
///
/// If the path is a normal file, this is the file name. If it's the path of a directory, this
/// is the directory name.
///
/// Returns [`None`] if the path terminates in `..`.
pub fn file_name(&self, path: &str) -> Option<String> {
Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
}

/// Helper to get a desktop resolver for delegation
fn desktop_resolver(&self) -> desktop::PathResolver<R> {
desktop::PathResolver(self.app.clone())
}

/// Helper to call the iOS plugin
fn call_resolve(&self, dir: &str) -> Result<PathBuf> {
let response = self
.plugin
.run_mobile_plugin::<PathResponse>(dir, ())
.map_err(|_| Error::UnknownPath)?;

match response.path {
Some(p) if !p.is_empty() => Ok(PathBuf::from(p)),
_ => Err(Error::UnknownPath),
}
}

/// Returns the path to the user's audio directory.
pub fn audio_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().audio_dir()
}

/// Returns the path to the user's cache directory.
pub fn cache_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().cache_dir()
}

/// Returns the path to the user's config directory.
pub fn config_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().config_dir()
}

/// Returns the path to the user's data directory.
pub fn data_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().data_dir()
}

/// Returns the path to the user's local data directory.
pub fn local_data_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().local_data_dir()
}

/// Returns the path to the user's document directory.
pub fn document_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().document_dir()
}

/// Returns the path to the user's download directory.
pub fn download_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().download_dir()
}

/// Returns the path to the user's picture directory.
pub fn picture_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().picture_dir()
}

/// Returns the path to the user's public directory.
pub fn public_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().public_dir()
}

/// Returns the path to the user's video dir
pub fn video_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().video_dir()
}

/// Returns the path to the resource directory of this app.
pub fn resource_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().resource_dir()
}

/// Returns the path to the suggested directory for your app's config files.
///
/// Resolves to [`config_dir`]`/${bundle_identifier}`.
pub fn app_config_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().app_config_dir()
}

/// Returns the path to the suggested directory for your app's data files.
///
/// Resolves to [`data_dir`]`/${bundle_identifier}`.
pub fn app_data_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().app_data_dir()
}

/// Returns the path to the suggested directory for your app's local data files.
///
/// Resolves to [`local_data_dir`]`/${bundle_identifier}`.
pub fn app_local_data_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().app_local_data_dir()
}

/// Returns the path to the suggested directory for your app's cache files.
///
/// Resolves to `FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!`
pub fn app_cache_dir(&self) -> Result<PathBuf> {
self.call_resolve("getCacheDir")
}

/// Returns the path to the app's documents directory.
///
/// Resolves to `FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.
pub fn app_documents_dir(&self) -> Result<PathBuf> {
self.call_resolve("getDocumentsDir")
}

/// Returns the path to the app's library directory.
///
/// Resolves to `FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!.
pub fn app_library_dir(&self) -> Result<PathBuf> {
self.call_resolve("getLibraryDir")
}

/// Returns the path to the app's temporary directory.
///
/// Resolves to `NSTemporaryDirectory()`
pub fn app_temp_dir(&self) -> Result<PathBuf> {
self.call_resolve("getTempDir")
}

/// Returns the path to the suggested directory for your app's log files.
pub fn app_log_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().app_log_dir()
}

/// A temporary directory. Resolves to [`std::env::temp_dir`].
pub fn temp_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().temp_dir()
}

/// Returns the path to the user's home directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$HOME`.
/// - **macOS:** Resolves to `$HOME`.
/// - **Windows:** Resolves to `{FOLDERID_Profile}`.
/// - **iOS**: Cannot be written to directly, use one of the app paths instead.
pub fn home_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().home_dir()
}

/// Returns the path to the user's desktop directory.
///
/// Not supported on iOS, returns error.
pub fn desktop_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().desktop_dir()
}

/// Returns the path to the user's executable directory.
///
/// Not supported on iOS, returns error.
pub fn executable_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().executable_dir()
}

/// Returns the path to the user's font directory.
///
/// Not supported on iOS, returns error.
pub fn font_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().font_dir()
}

/// Returns the path to the user's runtime directory.
///
/// Not supported on iOS, returns error.
pub fn runtime_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().runtime_dir()
}

/// Returns the path to the user's template directory.
///
/// Not supported on iOS, returns error.
pub fn template_dir(&self) -> Result<PathBuf> {
self.desktop_resolver().template_dir()
}
}
Loading