Claude Skill

desktop-mobile-tauri

Tauri 2.x mobile development - iOS via WKWebView, Android via Android WebView, mobile plugins, Swift/Kotlin native code, permissions, debugging

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_desktop-mobile-tauri_skills_desktop-mobile-tauri-3a51ef5.zip · 20 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-mobile-tauri/skills/desktop-mobile-tauri
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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 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:

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)] 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>

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.

No comments yet.

Reviews (0)

No reviews yet.

Related