desktop-mobile-tauri
Tauri 2.x mobile development - iOS via WKWebView, Android via Android WebView, mobile plugins, Swift/Kotlin native code, permissions, debugging
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-mobile-tauri/skills/desktop-mobile-tauri
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Tauri 2.x Mobile Development
Quick Guide: Tauri 2.x supports iOS (WKWebView) and Android (Android WebView) from the same codebase as desktop. Initialize with
tauri android init/tauri ios init, run withtauri android dev/tauri ios dev. Mobile-only plugins (biometric, barcode-scanner, NFC, haptics, geolocation) use#[cfg(mobile)]for conditional registration. Custom native code uses Swift classes extendingPluginon iOS and Kotlin classes annotated with@TauriPluginon Android. Every mobile plugin needs platform permissions (Info.plist keys on iOS, AndroidManifest.xml permissions on Android) in addition to Tauri capability grants.Current version: Tauri 2.x (stable). Mobile support is production-ready since Tauri 2.0 (2024).
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use #[cfg(mobile)] when registering mobile-only plugins -- registering them unconditionally breaks desktop builds)
(You MUST add platform permissions (Info.plist on iOS, AndroidManifest.xml on Android) in ADDITION to Tauri capability file permissions -- missing platform permissions cause silent failures or runtime crashes)
(You MUST use #[cfg_attr(mobile, tauri::mobile_entry_point)] on pub fn run() -- without it, the app cannot launch on mobile)
(You MUST run mobile dev commands (tauri ios dev, tauri android dev) instead of tauri dev for mobile targets -- tauri dev only targets desktop)
</critical_requirements>
Auto-detection: tauri android init, tauri ios init, tauri android dev, tauri ios dev, tauri-plugin-biometric, tauri-plugin-barcode-scanner, tauri-plugin-nfc, tauri-plugin-haptics, tauri-plugin-geolocation, #[cfg(mobile)], #[cfg(target_os = "android")], #[cfg(target_os = "ios")], mobile_entry_point, Info.plist, Info.ios.plist, AndroidManifest.xml, NSCameraUsageDescription, NSFaceIDUsageDescription, NSLocationWhenInUseUsageDescription, @TauriPlugin, Plugin Swift class, WKWebView, Invoke, InvokeArg, run_mobile_plugin, develop-mobile
When to use:
- Adding iOS or Android targets to a Tauri 2.x project
- Using mobile-specific plugins (biometric auth, barcode scanner, NFC, haptics, geolocation)
- Writing custom native plugin code in Swift (iOS) or Kotlin (Android)
- Configuring mobile platform permissions and capabilities
- Debugging on mobile simulators/emulators or physical devices
- Writing platform-conditional Rust code for mobile vs desktop
When NOT to use:
- Desktop-only Tauri development (use the desktop-framework-tauri skill)
- General Tauri concepts (commands, IPC, events, permissions, window management -- desktop skill covers these)
- Frontend framework patterns (component architecture, state management -- use respective framework skills)
- General Rust programming not related to Tauri mobile APIs
Key patterns covered:
- Mobile project initialization and prerequisites (examples/core.md)
- Mobile-specific plugin registration with
#[cfg(mobile)](examples/core.md) - Mobile plugin gallery: biometric, barcode-scanner, NFC, haptics, geolocation (examples/plugins.md)
- Custom Swift plugin development for iOS (examples/native-plugins.md)
- Custom Kotlin plugin development for Android (examples/native-plugins.md)
- Platform permissions: Info.plist, AndroidManifest.xml (examples/core.md)
- Mobile debugging: Safari Web Inspector, Chrome DevTools, logcat (examples/core.md)
Detailed resources:
- examples/core.md - Project setup, mobile plugin registration, permissions, platform-conditional code, debugging
- examples/plugins.md - Mobile-specific plugins (biometric, barcode, NFC, haptics, geolocation)
- examples/native-plugins.md - Custom Swift and Kotlin plugin development, calling Rust from mobile
- reference.md - CLI commands, prerequisites checklist, mobile plugin registry, permission reference
<decision_framework>
Decision Framework
Mobile Plugin Selection
Need device hardware access?
|-- Camera for scanning?
| +-- tauri-plugin-barcode-scanner (QR, EAN-13, etc.)
|-- Biometric authentication?
| +-- tauri-plugin-biometric (Face ID, fingerprint)
|-- NFC tags?
| +-- tauri-plugin-nfc (read/write NDEF tags)
|-- Vibration / haptic feedback?
| +-- tauri-plugin-haptics (impact, notification, selection feedback)
|-- GPS / location?
| +-- tauri-plugin-geolocation (position, altitude, heading, speed)
+-- Other device features?
+-- Check the Tauri plugin registry for mobile-compatible plugins
Desktop vs Mobile Plugin Registration
Is this plugin mobile-only?
|-- YES (biometric, barcode, NFC, haptics, geolocation)
| +-- Use #[cfg(mobile)] for registration
| +-- Use cfg(any(target_os = "android", target_os = "ios")) for Cargo deps
|-- NO (fs, dialog, store, notification, http, etc.)
| +-- Register unconditionally (works on both desktop and mobile)
+-- UNSURE
+-- Check plugin docs for "Supported Platforms" table
Permission Layering
Adding a mobile plugin?
|
+-- Step 1: Tauri capability file (src-tauri/capabilities/)
| +-- Add plugin permissions (e.g., "biometric:default")
+-- Step 2: iOS Info.plist (src-tauri/Info.ios.plist)
| +-- Add NS*UsageDescription keys for each permission
+-- Step 3: Android manifest (gen/android/.../AndroidManifest.xml)
| +-- Add <uses-permission> and <uses-feature> elements
+-- Step 4: Runtime permission request
+-- Use plugin's checkPermissions() / requestPermissions() API
See reference.md for CLI command reference and mobile prerequisites checklist.
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Registering mobile-only plugins without
#[cfg(mobile)]-- breaks desktop builds with missing native dependencies - Missing
#[cfg_attr(mobile, tauri::mobile_entry_point)]onrun()-- mobile app cannot launch - Adding Tauri capability permissions but forgetting platform permissions (Info.plist / AndroidManifest.xml) -- OS denies access at runtime
- Running
tauri devinstead oftauri ios dev/tauri android devfor mobile -- builds for desktop, not mobile - Using
@tauri-apps/api/tauriimport path (removed in v2 -- use@tauri-apps/api/core)
Medium Priority Issues:
- Not checking
isAvailable()before using hardware plugins (biometric, NFC) -- the device may lack hardware support - Running long operations on Android main thread in
@Commandmethods -- freezes the UI - Missing iOS
PrivacyInfo.xcprivacyfor App Store compliance -- Apple rejects apps without privacy manifests - Forgetting runtime permission requests (
checkPermissions()/requestPermissions()) -- iOS and Android require explicit user consent for camera, location, etc. - Not handling the
TAURI_DEV_HOSTenvironment variable in dev server config -- physical device cannot reach dev server
Common Mistakes:
- Editing files in
gen/android/orgen/apple/that get regenerated -- changes are lost on nexttauri android init/tauri ios init - Expecting identical webview rendering on iOS and Android -- WKWebView and Android WebView have different CSS/JS engine capabilities
- Forgetting to add Rust targets (
rustup target add aarch64-apple-ios aarch64-linux-android ...) -- compilation fails - Installing the Cargo crate without the npm package for mobile plugins -- TypeScript API unavailable
Gotchas & Edge Cases:
- iOS only on macOS:
tauri ios initandtauri ios devrequire macOS with Xcode installed - Android 16KB pages: For NDK < 28, you need
-C link-arg=-Wl,-z,max-page-size=16384in.cargo/config.tomlforaarch64-linux-android - Haptics inconsistency: No standard for vibration support on Android -- feedback APIs may not work on budget devices
- NFC on iOS: Requires iOS 14+ minimum deployment target and "Near Field Communication Tag Reading" capability in Xcode entitlements
--openflag lifecycle: When usingtauri ios dev --openortauri android dev --open, the Tauri CLI process must stay alive -- killing it breaks the build pipeline- Safe areas: Tauri does not provide built-in safe area handling -- use CSS
env(safe-area-inset-*)or a community plugin for edge-to-edge rendering - Orientation lock: No built-in Tauri API for forcing screen orientation -- requires platform-specific native code
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use #[cfg(mobile)] when registering mobile-only plugins -- registering them unconditionally breaks desktop builds)
(You MUST add platform permissions (Info.plist on iOS, AndroidManifest.xml on Android) in ADDITION to Tauri capability file permissions -- missing platform permissions cause silent failures or runtime crashes)
(You MUST use #[cfg_attr(mobile, tauri::mobile_entry_point)] on pub fn run() -- without it, the app cannot launch on mobile)
(You MUST run mobile dev commands (tauri ios dev, tauri android dev) instead of tauri dev for mobile targets -- tauri dev only targets desktop)
Failure to follow these rules will cause desktop build failures, runtime permission denials, or apps that cannot launch on mobile devices.
</critical_reminders>
Files (skills)
-
examples
-
core.md 12.4 KB
# Tauri Mobile - Core Examples > Project setup, mobile plugin registration, platform permissions, conditional compilation, and debugging. See [SKILL.md](../SKILL.md) for decision frameworks and red flags. See [plugins.md](plugins.md) for mobile-specific plugin usage. --- ## Prerequisites ### iOS (macOS only) ```sh # Install Xcode from Mac App Store (NOT just Command Line Tools) # Add iOS Rust targets rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim # Install CocoaPods brew install cocoapods ``` ### Android ```sh # Install Android Studio from https://developer.android.com/studio # Add Android Rust targets rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android ``` Set environment variables (add to shell profile): ```sh # Point to Android Studio's bundled JDK export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" # On Linux: export JAVA_HOME="/opt/android-studio/jbr" # Android SDK and NDK paths export ANDROID_HOME="$HOME/Library/Android/sdk" # On Linux: export ANDROID_HOME="$HOME/Android/Sdk" export NDK_HOME="$ANDROID_HOME/ndk/$(ls -1 $ANDROID_HOME/ndk | sort -V | tail -1)" ``` Use Android Studio SDK Manager to install: Android SDK Platform, Platform-Tools, NDK (Side by side), Build-Tools, Command-line Tools. --- ## Project Initialization ```sh # Initialize Android target (creates gen/android/ directory) npx tauri android init # Initialize iOS target (creates gen/apple/ directory) -- macOS only npx tauri ios init ``` After initialization, ensure `lib.rs` has the mobile entry point: ```rust // src-tauri/src/lib.rs #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![/* your commands */]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` **Why this pattern:** `tauri android init` and `tauri ios init` scaffold platform-specific project files. The `#[cfg_attr(mobile, tauri::mobile_entry_point)]` attribute generates the native entry point (Activity on Android, UIApplicationDelegate on iOS). Without it, the app compiles but cannot start. **Common mistake:** Running `tauri ios init` on Linux or Windows -- iOS development requires macOS with Xcode. --- ## Mobile Plugin Registration (Conditional) Mobile-only plugins must use `#[cfg(mobile)]` to avoid desktop build failures: ```toml # src-tauri/Cargo.toml -- conditional dependencies [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] tauri-plugin-biometric = "2" tauri-plugin-barcode-scanner = "2" tauri-plugin-nfc = "2" tauri-plugin-haptics = "2" tauri-plugin-geolocation = "2" ``` ```rust // src-tauri/src/lib.rs #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let mut builder = tauri::Builder::default(); // Register mobile-only plugins conditionally #[cfg(mobile)] { builder = builder .plugin(tauri_plugin_biometric::init()) .plugin(tauri_plugin_barcode_scanner::init()) .plugin(tauri_plugin_nfc::init()) .plugin(tauri_plugin_haptics::init()) .plugin(tauri_plugin_geolocation::init()); } // Cross-platform plugins -- register unconditionally builder .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_store::Builder::new().build()) .invoke_handler(tauri::generate_handler![/* commands */]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` **Why this pattern:** Mobile-only plugins depend on native iOS/Android libraries that are not available on desktop platforms. Conditional compilation prevents linker errors on desktop builds. Cross-platform plugins (fs, store, dialog, etc.) work on all platforms and should be registered unconditionally. **Common mistake:** Registering `tauri_plugin_biometric::init()` without `#[cfg(mobile)]` -- compiles on the mobile target but fails to link on desktop. --- ## Platform Permission Layering Mobile plugins require three layers of permissions: ### Layer 1: Tauri Capability File ```json { "$schema": "../gen/schemas/mobile-schema.json", "identifier": "mobile-capability", "description": "Permissions for mobile features", "platforms": ["android", "iOS"], "permissions": [ "core:default", "biometric:default", "barcode-scanner:allow-scan", "barcode-scanner:allow-cancel", "geolocation:allow-get-current-position", "geolocation:allow-watch-position", "geolocation:allow-check-permissions", "geolocation:allow-request-permissions", "nfc:default", "haptics:default" ] } ``` **Note:** Use `mobile-schema.json` (not `desktop-schema.json`) for mobile-specific capabilities. You can also set `"platforms": ["android", "iOS"]` to restrict the capability to mobile. ### Layer 2: iOS Info.plist ```xml <!-- src-tauri/Info.ios.plist --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <!-- Camera (barcode scanner) --> <key>NSCameraUsageDescription</key> <string>Required to scan barcodes and QR codes</string> <!-- Biometric (Face ID) --> <key>NSFaceIDUsageDescription</key> <string>Authenticate to access secure features</string> <!-- Location --> <key>NSLocationWhenInUseUsageDescription</key> <string>Required for location-based features</string> <!-- NFC --> <key>NFCReaderUsageDescription</key> <string>Required to read NFC tags</string> </dict> </plist> ``` ### Layer 3: Android Manifest ```xml <!-- gen/android/app/src/main/AndroidManifest.xml --> <!-- Add inside <manifest> tag --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.NFC" /> <!-- Optional: declare required hardware --> <uses-feature android:name="android.hardware.camera" android:required="true" /> <uses-feature android:name="android.hardware.location.gps" android:required="true" /> <uses-feature android:name="android.hardware.nfc" android:required="false" /> ``` **Why three layers:** Tauri capabilities control which plugin commands the webview can invoke. iOS Info.plist entries explain to the user WHY the app needs each permission. Android manifest entries declare which OS-level permissions the app requires. Missing any layer causes failures at different stages. **Common mistake:** Adding Tauri capability permissions but forgetting the iOS usage description string -- iOS silently denies the permission request. --- ## Runtime Permission Requests Many mobile features require explicit user consent at runtime: ```typescript import { checkPermissions, requestPermissions, } from "@tauri-apps/plugin-geolocation"; // Check current permission status const status = await checkPermissions(); if (status.location !== "granted") { // Request permission from the user const result = await requestPermissions(["location"]); if (result.location !== "granted") { // User denied -- handle gracefully return; } } // Now safe to use the API import { getCurrentPosition } from "@tauri-apps/plugin-geolocation"; const position = await getCurrentPosition(); ``` **Why this pattern:** On both iOS and Android, camera, location, and NFC require runtime user consent. Tauri plugins auto-generate `checkPermissions` and `requestPermissions` commands. Always check before using a protected API. --- ## Platform-Conditional Rust Code ### Conditional Dependencies ```toml # src-tauri/Cargo.toml # Mobile-only dependencies [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] tauri-plugin-biometric = "2" # Android-only dependencies [target.'cfg(target_os = "android")'.dependencies] jni = "0.21" # iOS-only dependencies [target.'cfg(target_os = "ios")'.dependencies] # iOS-specific crates here ``` ### Conditional Command Logic ```rust #[tauri::command] fn get_device_info() -> serde_json::Value { #[cfg(target_os = "android")] { serde_json::json!({ "platform": "android", "webview": "Android WebView" }) } #[cfg(target_os = "ios")] { serde_json::json!({ "platform": "ios", "webview": "WKWebView" }) } #[cfg(not(any(target_os = "android", target_os = "ios")))] { serde_json::json!({ "platform": "desktop", "webview": "system" }) } } ``` ### Desktop/Mobile Module Split (Plugin Pattern) ```rust // src-tauri/src/lib.rs #[cfg(mobile)] mod mobile; #[cfg(not(mobile))] mod desktop; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .setup(|app| { #[cfg(mobile)] mobile::setup(app)?; #[cfg(not(mobile))] desktop::setup(app)?; Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` **Why this pattern:** The module split keeps platform-specific code isolated. Rust's `cfg` attributes are compile-time -- code for other platforms is not included in the binary. --- ## Running and Debugging ### Dev Commands ```sh # iOS simulator (macOS only) npx tauri ios dev # Specific iOS device or simulator npx tauri ios dev 'iPhone 16' # Android emulator npx tauri android dev # Open in Xcode / Android Studio for native tooling npx tauri ios dev --open npx tauri android dev --open # Build for release npx tauri ios build npx tauri android build ``` ### WebView Debugging **iOS (Safari Web Inspector):** 1. Open Safari on Mac 2. Safari > Settings > Advanced > "Show features for web developers" 3. On physical device: Settings > Safari > Advanced > Web Inspector = ON 4. Run `tauri ios dev` 5. Safari > Develop menu > select device > inspect localhost **Android (Chrome DevTools):** 1. Enable USB Debugging on device (Settings > Developer Options) 2. Run `tauri android dev` 3. Open `chrome://inspect` in Chrome on your computer 4. Select your device and click "inspect" ### Native Log Debugging ```sh # Android: filter Tauri logs via logcat adb logcat | grep -i tauri # iOS: view logs in Xcode console when using --open npx tauri ios dev --open # Logs appear in Xcode's debug console ``` ### Physical Device Setup For physical devices, the dev server must be accessible over the local network. The Tauri CLI handles this automatically, but your frontend dev server must respect the `TAURI_DEV_HOST` environment variable: ```typescript // Frontend dev server config -- adapt to your build tool const DEV_PORT = 1420; const HMR_PORT = 1421; const serverConfig = { host: process.env.TAURI_DEV_HOST || "localhost", port: DEV_PORT, strictPort: true, hmr: process.env.TAURI_DEV_HOST ? { protocol: "ws", host: process.env.TAURI_DEV_HOST, port: HMR_PORT } : undefined, }; ``` **Why this pattern:** When developing on a physical device, the device connects to your computer's dev server over the network. `TAURI_DEV_HOST` provides the correct IP address for the device to reach. --- ## Mobile UI Considerations ### Safe Areas (Notch, Home Indicator) Tauri does not provide built-in safe area handling. Use CSS environment variables: ```css /* Respect device safe areas */ .app-container { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); } ``` Add the viewport meta tag for proper mobile rendering: ```html <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> ``` **Key point:** `viewport-fit=cover` enables edge-to-edge rendering, allowing content to extend under the notch and home indicator. Without `env(safe-area-inset-*)`, content will be obscured. --- ## Android 16KB Page Size Compliance For Google Play compliance with newer devices, add this when using NDK versions before 28: ```toml # .cargo/config.toml [target.aarch64-linux-android] rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] ``` **Why this matters:** Android devices with ARM64 processors may use 16KB memory pages. Without this flag, the app may crash on those devices. --- See [plugins.md](plugins.md) for mobile-specific plugin usage and [native-plugins.md](native-plugins.md) for custom Swift/Kotlin plugin development. -
native-plugins.md 13.3 KB
# Tauri Mobile - Native Plugin Development > Writing custom mobile plugins in Swift (iOS) and Kotlin (Android), handling arguments, calling Rust from mobile, and plugin lifecycle. See [core.md](core.md) for plugin registration. See [SKILL.md](../SKILL.md) for decision frameworks. --- ## Plugin Project Structure A Tauri plugin with mobile support has this structure: ``` tauri-plugin-example/ ├── src/ │ ├── lib.rs # Plugin entry point, shared logic │ ├── desktop.rs # Desktop implementation (Rust) │ ├── mobile.rs # Mobile implementation (delegates to native) │ └── commands.rs # Tauri command definitions ├── android/ │ └── src/main/java/com/plugin/example/ │ └── ExamplePlugin.kt # Kotlin implementation ├── ios/ │ └── Sources/ │ └── ExamplePlugin.swift # Swift implementation ├── guest-js/ │ └── index.ts # JavaScript/TypeScript bindings ├── Cargo.toml └── package.json ``` Initialize mobile support for an existing plugin: ```sh # Add Android native code scaffold npx tauri plugin android init # Add iOS native code scaffold npx tauri plugin ios init ``` --- ## iOS Plugin (Swift) ### Basic Plugin Class ```swift import Tauri import UIKit import WebKit class ExamplePlugin: Plugin { // Called when plugin is loaded @objc public override func load(webview: WKWebView) { // Plugin initialization -- access config, setup resources } @objc public func getDeviceInfo(_ invoke: Invoke) throws { let device = UIDevice.current invoke.resolve([ "name": device.name, "model": device.model, "systemVersion": device.systemVersion, ]) } @objc public func showNativeAlert(_ invoke: Invoke) throws { let args = try invoke.parseArgs(AlertArgs.self) DispatchQueue.main.async { let alert = UIAlertController( title: args.title, message: args.message, preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in invoke.resolve(["dismissed": true]) }) // Present from the root view controller if let rootVC = UIApplication.shared.keyWindow?.rootViewController { rootVC.present(alert, animated: true) } } } } ``` ### Argument Classes (Decodable) ```swift class AlertArgs: Decodable { let title: String // Required -- must be present in invoke payload let message: String // Required var timeout: Int? // Optional -- nullable type } // Nested arguments class UploadArgs: Decodable { let filePath: String var options: UploadOptions? } class UploadOptions: Decodable { var compress: Bool? var quality: Int? } ``` **Key point:** Required fields use `let`, optional fields use `var` with nullable type (`?`). Inner objects must also conform to `Decodable`. Field names must match the camelCase keys from the JavaScript invoke payload. ### iOS Permission Handling ```swift import Photos class ExamplePlugin: Plugin { @objc override func checkPermissions(_ invoke: Invoke) { let status = PHPhotoLibrary.authorizationStatus() let permission: String switch status { case .authorized, .limited: permission = "granted" case .denied, .restricted: permission = "denied" case .notDetermined: permission = "prompt" @unknown default: permission = "prompt" } invoke.resolve(["photos": permission]) } @objc public override func requestPermissions(_ invoke: Invoke) { PHPhotoLibrary.requestAuthorization { status in let permission = status == .authorized ? "granted" : "denied" invoke.resolve(["photos": permission]) } } } ``` **Key point:** Override `checkPermissions` and `requestPermissions` to integrate with Tauri's permission system. These are auto-generated as plugin commands that JavaScript can call. ### Emitting Events from iOS ```swift class ExamplePlugin: Plugin { @objc public func startMonitoring(_ invoke: Invoke) throws { // Emit events to JavaScript at any time trigger("status-changed", data: [ "status": "active", "timestamp": Date().timeIntervalSince1970, ]) invoke.resolve() } } ``` ```typescript // JavaScript listener import { addPluginListener } from "@tauri-apps/api/core"; const unlisten = await addPluginListener( "plugin:example", "status-changed", (event) => { console.log("Status:", event.status); }, ); ``` --- ## Android Plugin (Kotlin) ### Basic Plugin Class ```kotlin import android.app.Activity import android.webkit.WebView import app.tauri.annotation.Command import app.tauri.annotation.TauriPlugin import app.tauri.plugin.Invoke import app.tauri.plugin.JSObject import app.tauri.plugin.Plugin @TauriPlugin class ExamplePlugin(private val activity: Activity) : Plugin(activity) { override fun load(webView: WebView) { // Plugin initialization } @Command fun getDeviceInfo(invoke: Invoke) { val ret = JSObject() ret.put("manufacturer", android.os.Build.MANUFACTURER) ret.put("model", android.os.Build.MODEL) ret.put("version", android.os.Build.VERSION.SDK_INT) invoke.resolve(ret) } @Command fun showNativeToast(invoke: Invoke) { val args = invoke.parseArgs(ToastArgs::class.java) activity.runOnUiThread { android.widget.Toast.makeText(activity, args.message, android.widget.Toast.LENGTH_SHORT).show() } invoke.resolve() } } ``` ### Argument Classes (@InvokeArg) ```kotlin import app.tauri.annotation.InvokeArg @InvokeArg internal class ToastArgs { lateinit var message: String // Required -- lateinit crashes if missing var duration: Int = 0 // Optional with default } // Nested arguments @InvokeArg internal class UploadArgs { lateinit var filePath: String var options: UploadOptions? = null } @InvokeArg internal class UploadOptions { var compress: Boolean = false var quality: Int = 100 } ``` **Key point:** `lateinit var` for required fields (throws if missing). Regular `var` with default for optional fields. Inner classes also need `@InvokeArg`. Field names must match camelCase keys from the JavaScript invoke payload. ### Async Commands (Background Thread) ```kotlin import kotlinx.coroutines.* @TauriPlugin class ExamplePlugin(private val activity: Activity) : Plugin(activity) { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @Command fun fetchData(invoke: Invoke) { // IMPORTANT: Long-running operations must NOT run on main thread scope.launch { try { val result = performNetworkRequest() val ret = JSObject() ret.put("data", result) invoke.resolve(ret) } catch (e: Exception) { invoke.reject(e.message ?: "Unknown error") } } } override fun onDestroy() { scope.cancel() // Clean up coroutines } } ``` **Key point:** Android `@Command` methods run on the main thread by default. Network calls, file I/O, and heavy computation MUST be dispatched to a background thread or coroutine scope. Blocking the main thread causes an ANR (Application Not Responding) dialog. ### Android Permission Handling ```kotlin import android.Manifest import app.tauri.annotation.Permission import app.tauri.annotation.TauriPlugin @TauriPlugin( permissions = [ Permission( strings = [Manifest.permission.CAMERA], alias = "camera" ), Permission( strings = [ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, ], alias = "location" ) ] ) class ExamplePlugin(private val activity: Activity) : Plugin(activity) { // checkPermissions and requestPermissions are auto-generated } ``` **Key point:** Declare permissions in the `@TauriPlugin` annotation with string aliases. Tauri auto-generates `checkPermissions` and `requestPermissions` commands. The aliases are used in the JavaScript permission API. ### Android Lifecycle Events ```kotlin @TauriPlugin class ExamplePlugin(private val activity: Activity) : Plugin(activity) { override fun load(webView: WebView) { // Called when plugin initializes } override fun onNewIntent(intent: android.content.Intent) { // Called when activity is re-launched (deep links, notifications) val data = intent.data?.toString() if (data != null) { trigger("deep-link", JSObject().put("url", data)) } } override fun onResume() { // Activity resumed (returned from background) } override fun onPause() { // Activity going to background } override fun onDestroy() { // Clean up resources } } ``` ### Emitting Events from Android ```kotlin @TauriPlugin class ExamplePlugin(private val activity: Activity) : Plugin(activity) { @Command fun startMonitoring(invoke: Invoke) { val payload = JSObject() payload.put("status", "active") trigger("status-changed", payload) invoke.resolve() } } ``` --- ## Calling Rust from Mobile (Advanced) ### From Android (JNI) Load the compiled Rust library and call functions via JNI: ```kotlin @TauriPlugin class ExamplePlugin(private val activity: Activity) : Plugin(activity) { companion object { init { System.loadLibrary("app_lib") } } // Declare external Rust function private external fun processData(input: String): String? @Command fun process(invoke: Invoke) { val args = invoke.parseArgs(ProcessArgs::class.java) val result = processData(args.input) ?: "error" val ret = JSObject() ret.put("result", result) invoke.resolve(ret) } } ``` ```rust // src-tauri/src/lib.rs (or separate module) use jni::JNIEnv; use jni::objects::{JClass, JString}; use jni::sys::jstring; #[no_mangle] pub extern "system" fn Java_com_plugin_example_ExamplePlugin_processData( mut env: JNIEnv, _class: JClass, input: JString, ) -> jstring { let input: String = env.get_string(&input).unwrap().into(); let result = format!("Processed: {input}"); env.new_string(result).unwrap().into_raw() } ``` ```toml # Cargo.toml -- JNI dependency only on Android [target.'cfg(target_os = "android")'.dependencies] jni = "0.21" ``` **Key point:** JNI function names follow the pattern `Java_{package}_{class}_{method}` with dots replaced by underscores. The function must be `#[no_mangle]` and `extern "system"`. This is advanced usage -- most plugins work fine with the standard invoke/resolve pattern. ### From iOS (FFI) Call Rust functions from Swift using C-compatible FFI: ```swift class ExamplePlugin: Plugin { // Declare the Rust function @_silgen_name("process_data_ffi") private static func processDataFFI(_ input: UnsafePointer<CChar>) -> UnsafeMutablePointer<CChar>? @objc public func process(_ invoke: Invoke) throws { let args = try invoke.parseArgs(ProcessArgs.self) let resultPtr = args.input.withCString { ExamplePlugin.processDataFFI($0) } guard let ptr = resultPtr else { invoke.reject("Processing failed") return } let result = String(cString: ptr) // Free the Rust-allocated string free_rust_string(ptr) invoke.resolve(["result": result]) } } ``` ```rust use std::ffi::{CStr, CString}; use std::os::raw::c_char; #[no_mangle] pub unsafe extern "C" fn process_data_ffi(input: *const c_char) -> *mut c_char { let input = CStr::from_ptr(input).to_str().unwrap(); let result = format!("Processed: {input}"); CString::new(result).unwrap().into_raw() } #[no_mangle] pub unsafe extern "C" fn free_rust_string(ptr: *mut c_char) { if !ptr.is_null() { drop(CString::from_raw(ptr)); } } ``` **Key point:** Unlike JNI, iOS FFI uses standard C calling conventions. You must manually manage memory -- provide a `free_rust_string` function for strings allocated by Rust. `@_silgen_name` maps the Swift function to the Rust symbol name. --- ## Invoking Mobile Commands from Rust The `PluginHandle::run_mobile_plugin` API calls Swift/Kotlin code from Rust: ```rust use serde::{Deserialize, Serialize}; use tauri::Runtime; #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct CameraRequest { quality: usize, allow_edit: bool, } #[derive(Deserialize)] pub struct Photo { path: String, } pub struct ExamplePlugin<R: Runtime>(tauri::plugin::PluginHandle<R>); impl<R: Runtime> ExamplePlugin<R> { pub fn open_camera(&self, payload: CameraRequest) -> crate::Result<Photo> { self.0 .run_mobile_plugin("openCamera", payload) .map_err(Into::into) } } ``` **Key point:** `run_mobile_plugin` serializes the payload to JSON, passes it to the native mobile function, and deserializes the response. The command name (`"openCamera"`) must match the method name in Swift (`func openCamera`) or Kotlin (`fun openCamera`). --- See [core.md](core.md) for project setup and [plugins.md](plugins.md) for official mobile plugin usage. -
plugins.md 7.6 KB
# Tauri Mobile - Plugin Examples > Mobile-specific plugin setup and usage: biometric, barcode-scanner, NFC, haptics, geolocation. See [core.md](core.md) for plugin registration and permission layering. See [SKILL.md](../SKILL.md) for decision frameworks. --- ## Biometric Authentication Prompt the user for fingerprint or Face ID authentication. Mobile only (iOS, Android). ### Setup ```sh npx tauri add biometric ``` ```rust // src-tauri/src/lib.rs -- conditional registration #[cfg(mobile)] builder = builder.plugin(tauri_plugin_biometric::init()); ``` ```xml <!-- src-tauri/Info.ios.plist --> <key>NSFaceIDUsageDescription</key> <string>Authenticate to access secure features</string> ``` ```json // src-tauri/capabilities/mobile.json { "permissions": ["biometric:default"] } ``` ### Usage ```typescript import { authenticate } from "@tauri-apps/plugin-biometric"; // Authenticate the user try { await authenticate("Confirm your identity to proceed", { title: "Authentication Required", subtitle: "Verify to access sensitive data", confirmationRequired: true, allowDeviceCredential: true, // Fall back to PIN/password if biometric fails }); // Authentication succeeded } catch (error) { // Authentication failed, was cancelled, or hardware not available console.error("Biometric auth failed:", error); } ``` **Key point:** The `authenticate` function throws if hardware is unavailable or authentication fails -- wrap in try/catch. Setting `allowDeviceCredential: true` lets the user fall back to PIN/pattern/password. --- ## Barcode Scanner Scan QR codes, EAN-13, and other barcode formats using the device camera. Mobile only (iOS, Android). ### Setup ```sh npx tauri add barcode-scanner ``` ```rust #[cfg(mobile)] builder = builder.plugin(tauri_plugin_barcode_scanner::init()); ``` ```xml <!-- src-tauri/Info.ios.plist --> <key>NSCameraUsageDescription</key> <string>Required to scan barcodes and QR codes</string> ``` ```json // src-tauri/capabilities/mobile.json { "permissions": [ "barcode-scanner:allow-scan", "barcode-scanner:allow-cancel", "barcode-scanner:allow-check-permissions", "barcode-scanner:allow-request-permissions" ] } ``` ### Usage ```typescript import { scan, cancel, Format } from "@tauri-apps/plugin-barcode-scanner"; // Scan with camera overlay (opens separate camera view) const result = await scan({ formats: [Format.QR_CODE, Format.EAN_13], }); console.log("Scanned:", result.content); // Windowed mode: makes webview transparent, camera shows behind it const result2 = await scan({ windowed: true, formats: [Format.QR_CODE], }); // Cancel an ongoing scan await cancel(); ``` **Key point:** `windowed: true` makes the webview transparent so the camera feed shows behind your UI -- useful for custom scan overlays. Without it, a separate full-screen camera view opens. --- ## NFC (Near Field Communication) Read and write NFC tags. Mobile only (iOS, Android). Requires iOS 14+. ### Setup ```sh npx tauri add nfc ``` ```rust #[cfg(mobile)] builder = builder.plugin(tauri_plugin_nfc::init()); ``` ```xml <!-- src-tauri/Info.ios.plist --> <key>NFCReaderUsageDescription</key> <string>Required to read NFC tags</string> ``` On iOS, you must also enable the "Near Field Communication Tag Reading" capability in Xcode or add it to entitlements. ```json // src-tauri/capabilities/mobile.json { "permissions": ["nfc:default"] } ``` ### Usage ```typescript import { isAvailable, scan, write, textRecord, uriRecord, } from "@tauri-apps/plugin-nfc"; // Check NFC hardware availability const available = await isAvailable(); if (!available) { console.warn("NFC not available on this device"); return; } // Read an NFC tag const tag = await scan({ type: "tag", keepSessionAlive: false, }); console.log("Tag data:", tag); // Write to an NFC tag await write([uriRecord("https://example.com"), textRecord("Hello from Tauri")]); ``` **Key point:** NFC requires physical proximity. On iOS, a system NFC sheet appears during scanning. `keepSessionAlive: true` allows multiple reads without re-triggering the scan UI. --- ## Haptics (Vibration and Feedback) Provide tactile feedback on mobile devices. Mobile only (iOS, Android). ### Setup ```sh npx tauri add haptics ``` ```rust #[cfg(mobile)] builder = builder.plugin(tauri_plugin_haptics::init()); ``` ```json // src-tauri/capabilities/mobile.json { "permissions": [ "haptics:allow-vibrate", "haptics:allow-impact-feedback", "haptics:allow-notification-feedback", "haptics:allow-selection-feedback" ] } ``` ### Usage ```typescript import { vibrate, impactFeedback, notificationFeedback, selectionFeedback, } from "@tauri-apps/plugin-haptics"; // Simple vibration (duration in milliseconds) const VIBRATE_DURATION_MS = 100; await vibrate(VIBRATE_DURATION_MS); // Impact feedback (light, medium, heavy) await impactFeedback("medium"); // Notification feedback (success, warning, error) await notificationFeedback("success"); // Selection feedback (subtle tick for UI selection changes) await selectionFeedback(); ``` **Key point:** iOS maps these to UIKit haptic feedback generators (UIImpactFeedbackGenerator, etc.). Android vibration support varies by device -- budget phones may not support all feedback styles. No platform permissions needed beyond Tauri capability grants. --- ## Geolocation Get and track device position including altitude, heading, and speed. Works on mobile; limited desktop support. ### Setup ```sh npx tauri add geolocation ``` ```rust #[cfg(mobile)] builder = builder.plugin(tauri_plugin_geolocation::init()); ``` ```xml <!-- src-tauri/Info.ios.plist --> <key>NSLocationWhenInUseUsageDescription</key> <string>Required for location-based features</string> ``` ```xml <!-- gen/android/app/src/main/AndroidManifest.xml --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-feature android:name="android.hardware.location.gps" android:required="true" /> ``` ```json // src-tauri/capabilities/mobile.json { "permissions": [ "geolocation:allow-get-current-position", "geolocation:allow-watch-position", "geolocation:allow-check-permissions", "geolocation:allow-request-permissions" ] } ``` ### Usage ```typescript import { checkPermissions, requestPermissions, getCurrentPosition, watchPosition, } from "@tauri-apps/plugin-geolocation"; // Request location permission const status = await checkPermissions(); if (status.location !== "granted") { const result = await requestPermissions(["location"]); if (result.location !== "granted") { return; // User denied } } // Get current position (one-shot) const position = await getCurrentPosition(); console.log( `Lat: ${position.coords.latitude}, Lng: ${position.coords.longitude}`, ); console.log(`Altitude: ${position.coords.altitude}`); console.log(`Speed: ${position.coords.speed}`); // Watch position (continuous tracking) const watchId = await watchPosition( { enableHighAccuracy: true }, (position, error) => { if (error) { console.error("Location error:", error); return; } console.log( `Updated: ${position.coords.latitude}, ${position.coords.longitude}`, ); }, ); // Stop watching (important for battery life) // clearWatch(watchId); ``` **Key point:** Always request permissions before accessing location. `enableHighAccuracy: true` uses GPS (slower, more battery, more precise). Call `clearWatch()` when tracking is no longer needed -- continuous GPS tracking drains battery quickly. --- See [core.md](core.md) for the permission layering pattern and [native-plugins.md](native-plugins.md) for writing custom plugins.
-
-
reference.md 8.2 KB
# Tauri Mobile Reference > Quick-lookup tables, CLI commands, prerequisites checklist, and mobile plugin registry. See [SKILL.md](SKILL.md) for decision frameworks and red flags. See [examples/core.md](examples/core.md) for full code examples. --- ## Mobile CLI Commands | Command | Purpose | | ------------------------------- | ------------------------------------------------ | | `npx tauri android init` | Initialize Android project (creates gen/android) | | `npx tauri ios init` | Initialize iOS project (creates gen/apple) | | `npx tauri android dev` | Run on Android emulator/device | | `npx tauri ios dev` | Run on iOS simulator/device | | `npx tauri android dev --open` | Open Android Studio for native debugging | | `npx tauri ios dev --open` | Open Xcode for native debugging | | `npx tauri ios dev 'iPhone 16'` | Target specific simulator/device | | `npx tauri android build` | Build release APK/AAB | | `npx tauri ios build` | Build release IPA | | `npx tauri plugin android init` | Add Android support to a plugin | | `npx tauri plugin ios init` | Add iOS support to a plugin | --- ## Prerequisites Checklist ### iOS (macOS only) - [ ] Xcode installed (full app, not just Command Line Tools) - [ ] CocoaPods installed (`brew install cocoapods`) - [ ] Rust targets added: - `rustup target add aarch64-apple-ios` - `rustup target add x86_64-apple-ios` - `rustup target add aarch64-apple-ios-sim` - [ ] `npx tauri ios init` run in project ### Android - [ ] Android Studio installed - [ ] SDK Platform, Platform-Tools, NDK, Build-Tools installed via SDK Manager - [ ] `JAVA_HOME` set to Android Studio bundled JDK - [ ] `ANDROID_HOME` set to Android SDK path - [ ] `NDK_HOME` set to NDK path within SDK - [ ] Rust targets added: - `rustup target add aarch64-linux-android` - `rustup target add armv7-linux-androideabi` - `rustup target add i686-linux-android` - `rustup target add x86_64-linux-android` - [ ] `npx tauri android init` run in project --- ## Mobile-Specific Plugin Registry | Plugin | Cargo Crate | NPM Package | Platform | | --------------- | ------------------------------ | ------------------------------------ | ----------- | | Biometric | `tauri-plugin-biometric` | `@tauri-apps/plugin-biometric` | iOS/Android | | Barcode Scanner | `tauri-plugin-barcode-scanner` | `@tauri-apps/plugin-barcode-scanner` | iOS/Android | | NFC | `tauri-plugin-nfc` | `@tauri-apps/plugin-nfc` | iOS/Android | | Haptics | `tauri-plugin-haptics` | `@tauri-apps/plugin-haptics` | iOS/Android | | Geolocation | `tauri-plugin-geolocation` | `@tauri-apps/plugin-geolocation` | iOS/Android | ### Cross-Platform Plugins (also work on mobile) | Plugin | Cargo Crate | Mobile Notes | | ------------ | --------------------------- | -------------------------------------------------- | | File System | `tauri-plugin-fs` | Needs AndroidManifest storage permissions | | Dialog | `tauri-plugin-dialog` | File selection on both; folder picker desktop-only | | Store | `tauri-plugin-store` | Works unchanged on mobile | | Notification | `tauri-plugin-notification` | Needs runtime permission on iOS | | HTTP | `tauri-plugin-http` | Works unchanged, bypasses CORS | | Log | `tauri-plugin-log` | Outputs to logcat (Android) / os_log (iOS) | | Process | `tauri-plugin-process` | Works unchanged on mobile | | OS | `tauri-plugin-os` | Returns mobile platform info | ### Desktop-Only Plugins (do NOT work on mobile) | Plugin | Why Desktop Only | | --------------- | ------------------------------------------------------ | | Shell | Mobile OS sandboxing prevents spawning child processes | | Autostart | No concept of "launch on login" on mobile | | Global Shortcut | No system-wide keyboard shortcuts on mobile | | Window State | Mobile apps are single-window | --- ## Platform Permission Reference ### iOS Info.plist Keys | Plugin | Key | Example Value | | --------------- | ------------------------------------- | ----------------------------------------- | | Biometric | `NSFaceIDUsageDescription` | "Authenticate to access secure features" | | Barcode Scanner | `NSCameraUsageDescription` | "Required to scan barcodes and QR codes" | | NFC | `NFCReaderUsageDescription` | "Required to read NFC tags" | | Geolocation | `NSLocationWhenInUseUsageDescription` | "Required for location-based features" | | File System | (PrivacyInfo.xcprivacy) | NSPrivacyAccessedAPICategoryFileTimestamp | ### Android Manifest Permissions | Plugin | Permission | | --------------- | ------------------------------------------- | | Barcode Scanner | `android.permission.CAMERA` | | Geolocation | `android.permission.ACCESS_FINE_LOCATION` | | Geolocation | `android.permission.ACCESS_COARSE_LOCATION` | | NFC | `android.permission.NFC` | | File System | `android.permission.READ_EXTERNAL_STORAGE` | | File System | `android.permission.WRITE_EXTERNAL_STORAGE` | --- ## Rust Conditional Compilation Quick Reference | Condition | Use Case | | ------------------------------------------------ | -------------------------------------- | | `#[cfg(mobile)]` | Any mobile platform (iOS + Android) | | `#[cfg(not(mobile))]` | Desktop only | | `#[cfg(target_os = "android")]` | Android only | | `#[cfg(target_os = "ios")]` | iOS only | | `#[cfg(any(target_os = "android", ...))]` | Explicit multi-platform list | | `#[cfg_attr(mobile, tauri::mobile_entry_point)]` | Mobile entry point (required on run()) | ### Cargo.toml Conditional Dependencies ```toml # Mobile-only [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] tauri-plugin-biometric = "2" # Android-only [target.'cfg(target_os = "android")'.dependencies] jni = "0.21" ``` --- ## Mobile Debugging Quick Reference | Platform | Tool | Access Method | | -------- | -------------------- | --------------------------------------- | | iOS | Safari Web Inspector | Safari > Develop > [device] > localhost | | iOS | Xcode Console | `npx tauri ios dev --open` | | Android | Chrome DevTools | `chrome://inspect` in Chrome | | Android | Logcat | `adb logcat \| grep -i tauri` | | Both | tauri-plugin-log | Structured logging across all platforms | ### Physical Device Requirements | Platform | Requirement | | -------- | ---------------------------------------------------- | | iOS | Enable Web Inspector in Settings > Safari > Advanced | | Android | Enable USB Debugging in Developer Options | | Both | Dev server must be accessible on local network | | Both | Frontend dev server must respect `TAURI_DEV_HOST` | --- ## See Also - [Tauri Mobile Development](https://v2.tauri.app/develop/) - [Tauri Mobile Plugin Development](https://v2.tauri.app/develop/plugins/develop-mobile/) - [Tauri Plugin Registry](https://v2.tauri.app/plugin/) - [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/) -
SKILL.md 17.6 KB
--- name: desktop-mobile-tauri description: Tauri 2.x mobile development - iOS via WKWebView, Android via Android WebView, mobile plugins, Swift/Kotlin native code, permissions, debugging --- # Tauri 2.x Mobile Development > **Quick Guide:** Tauri 2.x supports iOS (WKWebView) and Android (Android WebView) from the same codebase as desktop. Initialize with `tauri android init` / `tauri ios init`, run with `tauri android dev` / `tauri ios dev`. Mobile-only plugins (biometric, barcode-scanner, NFC, haptics, geolocation) use `#[cfg(mobile)]` for conditional registration. Custom native code uses Swift classes extending `Plugin` on iOS and Kotlin classes annotated with `@TauriPlugin` on Android. Every mobile plugin needs platform permissions (Info.plist keys on iOS, AndroidManifest.xml permissions on Android) in addition to Tauri capability grants. > > **Current version:** Tauri 2.x (stable). Mobile support is production-ready since Tauri 2.0 (2024). --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `#[cfg(mobile)]` when registering mobile-only plugins -- registering them unconditionally breaks desktop builds)** **(You MUST add platform permissions (Info.plist on iOS, AndroidManifest.xml on Android) in ADDITION to Tauri capability file permissions -- missing platform permissions cause silent failures or runtime crashes)** **(You MUST use `#[cfg_attr(mobile, tauri::mobile_entry_point)]` on `pub fn run()` -- without it, the app cannot launch on mobile)** **(You MUST run mobile dev commands (`tauri ios dev`, `tauri android dev`) instead of `tauri dev` for mobile targets -- `tauri dev` only targets desktop)** </critical_requirements> --- **Auto-detection:** tauri android init, tauri ios init, tauri android dev, tauri ios dev, tauri-plugin-biometric, tauri-plugin-barcode-scanner, tauri-plugin-nfc, tauri-plugin-haptics, tauri-plugin-geolocation, #[cfg(mobile)], #[cfg(target_os = "android")], #[cfg(target_os = "ios")], mobile_entry_point, Info.plist, Info.ios.plist, AndroidManifest.xml, NSCameraUsageDescription, NSFaceIDUsageDescription, NSLocationWhenInUseUsageDescription, @TauriPlugin, Plugin Swift class, WKWebView, Invoke, InvokeArg, run_mobile_plugin, develop-mobile **When to use:** - Adding iOS or Android targets to a Tauri 2.x project - Using mobile-specific plugins (biometric auth, barcode scanner, NFC, haptics, geolocation) - Writing custom native plugin code in Swift (iOS) or Kotlin (Android) - Configuring mobile platform permissions and capabilities - Debugging on mobile simulators/emulators or physical devices - Writing platform-conditional Rust code for mobile vs desktop **When NOT to use:** - Desktop-only Tauri development (use the desktop-framework-tauri skill) - General Tauri concepts (commands, IPC, events, permissions, window management -- desktop skill covers these) - Frontend framework patterns (component architecture, state management -- use respective framework skills) - General Rust programming not related to Tauri mobile APIs **Key patterns covered:** - Mobile project initialization and prerequisites ([examples/core.md](examples/core.md)) - Mobile-specific plugin registration with `#[cfg(mobile)]` ([examples/core.md](examples/core.md)) - Mobile plugin gallery: biometric, barcode-scanner, NFC, haptics, geolocation ([examples/plugins.md](examples/plugins.md)) - Custom Swift plugin development for iOS ([examples/native-plugins.md](examples/native-plugins.md)) - Custom Kotlin plugin development for Android ([examples/native-plugins.md](examples/native-plugins.md)) - Platform permissions: Info.plist, AndroidManifest.xml ([examples/core.md](examples/core.md)) - Mobile debugging: Safari Web Inspector, Chrome DevTools, logcat ([examples/core.md](examples/core.md)) **Detailed resources:** - [examples/core.md](examples/core.md) - Project setup, mobile plugin registration, permissions, platform-conditional code, debugging - [examples/plugins.md](examples/plugins.md) - Mobile-specific plugins (biometric, barcode, NFC, haptics, geolocation) - [examples/native-plugins.md](examples/native-plugins.md) - Custom Swift and Kotlin plugin development, calling Rust from mobile - [reference.md](reference.md) - CLI commands, prerequisites checklist, mobile plugin registry, permission reference --- <philosophy> ## Philosophy Tauri mobile extends the same Rust backend + webview frontend architecture to iOS and Android. The key difference: mobile apps run in the OS native webview (WKWebView on iOS, Android WebView on Android) and can access device hardware through mobile-specific plugins. Your existing Tauri desktop code (commands, state, events) works on mobile without changes -- you add mobile support incrementally. **When Tauri mobile is the right choice:** - You already have a Tauri desktop app and want to share the codebase with mobile - You want a single codebase for desktop + mobile with web frontend skills - You need native device features (camera, biometrics, NFC) accessible via plugins - You want small app sizes compared to alternatives that bundle their own webview **When Tauri mobile may NOT be the right choice:** - You need pixel-perfect native UI (Tauri renders web content, not native widgets) - You need features that require a consistent browser engine (Tauri uses the OS webview, which varies) - Your app is mobile-only with no desktop plans (native mobile frameworks may be more appropriate) - You need advanced mobile-specific APIs not yet covered by Tauri plugins </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Mobile Project Initialization Initialize mobile targets in an existing Tauri project. Each platform requires its own init step. ```sh # Initialize Android target (generates gen/android/ project) npx tauri android init # Initialize iOS target (generates gen/apple/ project) -- macOS only npx tauri ios init ``` After init, add the mobile entry point attribute to your `run()` function: ```rust // src-tauri/src/lib.rs #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![/* commands */]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` **Key point:** `#[cfg_attr(mobile, tauri::mobile_entry_point)]` is required for mobile builds. Without it, the app cannot start on iOS or Android. The attribute is a no-op on desktop, so it is safe to always include. See [examples/core.md](examples/core.md) for prerequisites and environment setup. --- ### Pattern 2: Mobile Plugin Registration with #[cfg(mobile)] Mobile-only plugins must be conditionally registered to avoid breaking desktop builds. ```rust #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let mut builder = tauri::Builder::default(); // Mobile-only plugins -- conditional registration #[cfg(mobile)] { builder = builder .plugin(tauri_plugin_biometric::init()) .plugin(tauri_plugin_barcode_scanner::init()) .plugin(tauri_plugin_nfc::init()) .plugin(tauri_plugin_haptics::init()) .plugin(tauri_plugin_geolocation::init()); } builder .invoke_handler(tauri::generate_handler![/* commands */]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` **Key point:** Using `#[cfg(mobile)]` ensures these plugins are only compiled and registered on iOS/Android. The Cargo dependencies should also be conditional. See [examples/core.md](examples/core.md) for Cargo.toml configuration. --- ### Pattern 3: Platform-Conditional Rust Code Use `#[cfg(target_os)]` for platform-specific logic in commands or setup. ```rust #[tauri::command] fn get_platform_info() -> String { #[cfg(target_os = "android")] { "Running on Android".to_string() } #[cfg(target_os = "ios")] { "Running on iOS".to_string() } #[cfg(not(any(target_os = "android", target_os = "ios")))] { "Running on desktop".to_string() } } ``` **Key point:** `#[cfg(mobile)]` is shorthand for `#[cfg(any(target_os = "android", target_os = "ios"))]`. Use the specific `target_os` when behavior differs between Android and iOS. See [examples/core.md](examples/core.md) for conditional dependency examples. --- ### Pattern 4: Platform Permissions (Info.plist + AndroidManifest.xml) Mobile plugins require two layers of permissions: Tauri capability file grants AND native platform permission declarations. ```xml <!-- src-tauri/Info.ios.plist (iOS) --> <key>NSCameraUsageDescription</key> <string>Required to scan barcodes</string> <key>NSFaceIDUsageDescription</key> <string>Authenticate to access secure features</string> <key>NSLocationWhenInUseUsageDescription</key> <string>Required for location-based features</string> ``` ```xml <!-- gen/android/app/src/main/AndroidManifest.xml (Android) --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-feature android:name="android.hardware.location.gps" android:required="true" /> ``` **Key point:** Missing platform permissions cause silent failures or OS-level denials, even when Tauri capabilities are correctly configured. iOS needs usage description strings explaining WHY the app needs each permission. See [examples/core.md](examples/core.md) for the full permission layering pattern. --- ### Pattern 5: Custom Swift Plugin (iOS) Write native iOS code by extending the Tauri `Plugin` class in Swift. ```swift import Tauri import WebKit class MyPlugin: Plugin { @objc public func doSomething(_ invoke: Invoke) throws { let args = try invoke.parseArgs(DoSomethingArgs.self) // Native iOS API calls here invoke.resolve(["result": "success"]) } } class DoSomethingArgs: Decodable { let input: String var optional: Bool? } ``` **Key point:** Methods must have `@objc` attribute and accept an `Invoke` parameter. Arguments are parsed via `Decodable` classes. Use `invoke.resolve()` to return data or `invoke.reject()` to return errors. See [examples/native-plugins.md](examples/native-plugins.md) for complete examples. --- ### Pattern 6: Custom Kotlin Plugin (Android) Write native Android code with `@TauriPlugin` annotation and `@Command` methods. ```kotlin import app.tauri.annotation.Command import app.tauri.annotation.InvokeArg import app.tauri.annotation.TauriPlugin import app.tauri.plugin.Invoke import app.tauri.plugin.Plugin @InvokeArg internal class DoSomethingArgs { lateinit var input: String var optional: Boolean = false } @TauriPlugin class MyPlugin(private val activity: Activity) : Plugin(activity) { @Command fun doSomething(invoke: Invoke) { val args = invoke.parseArgs(DoSomethingArgs::class.java) val ret = JSObject() ret.put("result", "success") invoke.resolve(ret) } } ``` **Key point:** Commands annotated with `@Command` run on the main thread by default. Long-running operations must use coroutines or background threads to avoid freezing the UI. Arguments use `@InvokeArg` annotation with `lateinit var` for required fields. See [examples/native-plugins.md](examples/native-plugins.md) for async patterns and Rust interop. --- ### Pattern 7: Mobile Development and Debugging Run and debug on mobile devices/simulators. ```sh # Run on iOS simulator (macOS only) npx tauri ios dev # Run on specific iOS device/simulator npx tauri ios dev 'iPhone 16' # Run on Android emulator npx tauri android dev # Open in Xcode / Android Studio for native debugging npx tauri ios dev --open npx tauri android dev --open ``` **Debugging approaches:** - **iOS:** Safari > Develop menu > select device > inspect localhost - **Android:** `chrome://inspect` in Chrome > select connected device - **Rust logs:** Use `tauri-plugin-log` for structured logging across platforms - **Native logs:** Xcode console (iOS) / `adb logcat` (Android) **Key point:** The `--open` flag launches the IDE but the Tauri CLI process must stay running. For physical devices, the dev server must be reachable on the local network -- the CLI handles this via `TAURI_DEV_HOST`. See [examples/core.md](examples/core.md) for physical device setup. </patterns> --- <decision_framework> ## Decision Framework ### Mobile Plugin Selection ``` Need device hardware access? |-- Camera for scanning? | +-- tauri-plugin-barcode-scanner (QR, EAN-13, etc.) |-- Biometric authentication? | +-- tauri-plugin-biometric (Face ID, fingerprint) |-- NFC tags? | +-- tauri-plugin-nfc (read/write NDEF tags) |-- Vibration / haptic feedback? | +-- tauri-plugin-haptics (impact, notification, selection feedback) |-- GPS / location? | +-- tauri-plugin-geolocation (position, altitude, heading, speed) +-- Other device features? +-- Check the Tauri plugin registry for mobile-compatible plugins ``` ### Desktop vs Mobile Plugin Registration ``` Is this plugin mobile-only? |-- YES (biometric, barcode, NFC, haptics, geolocation) | +-- Use #[cfg(mobile)] for registration | +-- Use cfg(any(target_os = "android", target_os = "ios")) for Cargo deps |-- NO (fs, dialog, store, notification, http, etc.) | +-- Register unconditionally (works on both desktop and mobile) +-- UNSURE +-- Check plugin docs for "Supported Platforms" table ``` ### Permission Layering ``` Adding a mobile plugin? | +-- Step 1: Tauri capability file (src-tauri/capabilities/) | +-- Add plugin permissions (e.g., "biometric:default") +-- Step 2: iOS Info.plist (src-tauri/Info.ios.plist) | +-- Add NS*UsageDescription keys for each permission +-- Step 3: Android manifest (gen/android/.../AndroidManifest.xml) | +-- Add <uses-permission> and <uses-feature> elements +-- Step 4: Runtime permission request +-- Use plugin's checkPermissions() / requestPermissions() API ``` See [reference.md](reference.md) for CLI command reference and mobile prerequisites checklist. </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Registering mobile-only plugins without `#[cfg(mobile)]` -- breaks desktop builds with missing native dependencies - Missing `#[cfg_attr(mobile, tauri::mobile_entry_point)]` on `run()` -- mobile app cannot launch - Adding Tauri capability permissions but forgetting platform permissions (Info.plist / AndroidManifest.xml) -- OS denies access at runtime - Running `tauri dev` instead of `tauri ios dev` / `tauri android dev` for mobile -- builds for desktop, not mobile - Using `@tauri-apps/api/tauri` import path (removed in v2 -- use `@tauri-apps/api/core`) **Medium Priority Issues:** - Not checking `isAvailable()` before using hardware plugins (biometric, NFC) -- the device may lack hardware support - Running long operations on Android main thread in `@Command` methods -- freezes the UI - Missing iOS `PrivacyInfo.xcprivacy` for App Store compliance -- Apple rejects apps without privacy manifests - Forgetting runtime permission requests (`checkPermissions()` / `requestPermissions()`) -- iOS and Android require explicit user consent for camera, location, etc. - Not handling the `TAURI_DEV_HOST` environment variable in dev server config -- physical device cannot reach dev server **Common Mistakes:** - Editing files in `gen/android/` or `gen/apple/` that get regenerated -- changes are lost on next `tauri android init` / `tauri ios init` - Expecting identical webview rendering on iOS and Android -- WKWebView and Android WebView have different CSS/JS engine capabilities - Forgetting to add Rust targets (`rustup target add aarch64-apple-ios aarch64-linux-android ...`) -- compilation fails - Installing the Cargo crate without the npm package for mobile plugins -- TypeScript API unavailable **Gotchas & Edge Cases:** - **iOS only on macOS:** `tauri ios init` and `tauri ios dev` require macOS with Xcode installed - **Android 16KB pages:** For NDK < 28, you need `-C link-arg=-Wl,-z,max-page-size=16384` in `.cargo/config.toml` for `aarch64-linux-android` - **Haptics inconsistency:** No standard for vibration support on Android -- feedback APIs may not work on budget devices - **NFC on iOS:** Requires iOS 14+ minimum deployment target and "Near Field Communication Tag Reading" capability in Xcode entitlements - **`--open` flag lifecycle:** When using `tauri ios dev --open` or `tauri android dev --open`, the Tauri CLI process must stay alive -- killing it breaks the build pipeline - **Safe areas:** Tauri does not provide built-in safe area handling -- use CSS `env(safe-area-inset-*)` or a community plugin for edge-to-edge rendering - **Orientation lock:** No built-in Tauri API for forcing screen orientation -- requires platform-specific native code </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `#[cfg(mobile)]` when registering mobile-only plugins -- registering them unconditionally breaks desktop builds)** **(You MUST add platform permissions (Info.plist on iOS, AndroidManifest.xml on Android) in ADDITION to Tauri capability file permissions -- missing platform permissions cause silent failures or runtime crashes)** **(You MUST use `#[cfg_attr(mobile, tauri::mobile_entry_point)]` on `pub fn run()` -- without it, the app cannot launch on mobile)** **(You MUST run mobile dev commands (`tauri ios dev`, `tauri android dev`) instead of `tauri dev` for mobile targets -- `tauri dev` only targets desktop)** **Failure to follow these rules will cause desktop build failures, runtime permission denials, or apps that cannot launch on mobile devices.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.