--- url: /guide/AppIntent.md --- # AppIntent `AppIntentManager` is used to **register and manage AppIntents** in the **Scripting** app. It serves as the core mechanism for executing script logic behind controls in **Widgets**, **Live Activities**, and **ControlWidgets**. All `AppIntent`s **must** be defined in the `app_intents.tsx` file. When an intent is executed, the script runs in the `"app_intents"` environment (`Script.env === "app_intents"`). Once registered, these intents can be triggered by **Button** and **Toggle** controls within Widgets, Live Activities, or ControlWidgets, allowing users to define interactive behavior via script. *** ## 1. Type Definitions ### `AppIntent` Represents a concrete intent instance with parameters and metadata. | Property | Type | Description | | ---------- | ------------------- | -------------------------------------------------------------------------- | | `script` | `string` | The internal script path. Automatically generated by the system. | | `name` | `string` | The name of the AppIntent. Must be unique. | | `protocol` | `AppIntentProtocol` | The protocol the intent conforms to (e.g., general, audio, Live Activity). | | `params` | `T` | The parameters to be passed when the intent is executed. | *** ### `AppIntentFactory` A **factory function** that creates an `AppIntent` instance with specified parameters. ```ts type AppIntentFactory = (params: T) => AppIntent ``` *** ### `AppIntentPerform` A function that handles intent execution logic asynchronously. ```ts type AppIntentPerform = (params: T) => Promise ``` *** ### `AppIntentProtocol` An enumeration that defines the behavior type of the intent. | Enum Value | Description | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AppIntent` (0) | A general-purpose AppIntent for typical operations. | | `AudioPlaybackIntent` (1) | An intent that plays, pauses, or otherwise modifies audio playback. | | `AudioRecordingIntent` (2) | _(iOS 18.0+)_ An intent that starts, stops, or modifies audio recording. **Note**: On iOS/iPadOS, when using the `AudioRecordingIntent` protocol, you must start a **Live Activity** at the beginning of the recording and keep it active for the entire session. If you don't, the recording will be automatically stopped. | | `LiveActivityIntent` (3) | An intent that starts, pauses, or modifies a Live Activity. | *** ## 2. `AppIntentManager` Class ### `AppIntentManager.register(options): AppIntentFactory` Registers a new `AppIntent` by specifying its name, protocol, and perform logic. When a control (e.g., Button or Toggle) triggers the intent, the associated `perform` function is called. ```ts static register(options: { name: string; protocol: AppIntentProtocol; perform: AppIntentPerform; }): AppIntentFactory ``` #### Parameters: | Property | Type | Description | | ---------- | --------------------- | ------------------------------------------------------------------------------------------------------------------ | | `name` | `string` | A unique identifier for the AppIntent. | | `protocol` | `AppIntentProtocol` | The protocol this intent implements. | | `perform` | `AppIntentPerform` | The asynchronous function executed when the intent is triggered. The `params` argument is passed from the control. | #### Returns: - **`AppIntentFactory`**: A factory function that creates an `AppIntent` instance with the specified parameters. #### Example: ```tsx // app_intents.tsx export const ToggleDoorIntent = AppIntentManager.register({ name: "ToggleDoorIntent", protocol: AppIntentProtocol.AppIntent, perform: async ({ id, newState }: { id: string; newState: boolean }) => { // Custom logic: toggle the door state await setDoorState(id, newState) // Notify UI to refresh toggle state ControlWidget.reloadToggles() } }) ``` In a control view file (e.g., `control_widget_toggle.tsx`): ```tsx ControlWidget.present( ) ``` In a widget file (`widget.tsx`): ```tsx ``` *** ## 3. Execution Environment All AppIntents registered via `AppIntentManager` are executed in the `"app_intents"` environment. This allows safe use of APIs suitable for background execution, such as: - Fetching data from the network - Controlling Live Activities - Triggering control view refreshes *** ## 4. Best Practices 1. **Centralized Definitions**: All AppIntents **must** be defined in `app_intents.tsx` for discoverability and maintainability. 2. **Strong Typing**: Define explicit parameter types `T` for both `perform` and control usage to benefit from type checking and autocomplete. 3. **Choose the Right Protocol**: - General operation → `AppIntent` - Audio playback → `AudioPlaybackIntent` - Audio recording → `AudioRecordingIntent` _(requires iOS 18+, with Live Activity)_ - Live Activity control → `LiveActivityIntent` 4. **Trigger UI Updates**: If the intent modifies a UI state (e.g., toggle), call: - `ControlWidget.reloadButtons()` - `ControlWidget.reloadToggles()` - `Widget.reloadAll()` depending on where the control is hosted. --- url: /guide/Assistant/Assistant Conversation APIs.md --- # Assistant Conversation APIs The Conversation APIs are used to **start, control, and present a system-hosted Assistant chat session**. A conversation corresponds to a **fully managed chat page**, where Scripting handles the UI, streaming output, provider selection, and message lifecycle. Key differences from other Assistant APIs: - Conversation APIs are designed for **interactive chat experiences** - UI, streaming, and message handling are managed by the system - Developers control **when the conversation starts, ends, and is shown** *** ## Conversation Lifecycle A typical conversation follows this lifecycle: 1. `startConversation` — create a conversation (optionally auto-start) 2. `present` — display the Assistant chat page 3. User interacts with the Assistant 4. `dismiss` — temporarily hide the chat page (conversation continues) 5. `present` — show the same conversation again 6. `stopConversation` — terminate the conversation and release resources Important rules: - **Only one active conversation can exist at a time** - Calling `startConversation` while a conversation is active throws an error - Calling `stopConversation` automatically calls `dismiss` *** ## startConversation ### API Definition ```ts function startConversation(options: { message: string images?: UIImage[] autoStart?: boolean systemPrompt?: string modelId?: string provider?: Provider }): Promise ``` *** ### Parameters #### options.message - Type: `string` - Required - The **initial user message** of the conversation - Equivalent to the first user input in the chat UI *** #### options.images (optional) - Type: `UIImage[]` - Sent together with the initial message - Common use cases: - Image analysis - Starting a conversation from a photo or screenshot *** #### options.autoStart (optional) - Type: `boolean` - Default: `false` Behavior: - `true` - The assistant immediately starts generating a reply - `false` - The conversation is created but not sent automatically - Typically used when the user should press “Send” manually *** #### options.systemPrompt (optional) - Type: `string` Behavior: - If omitted: - The built-in Scripting Assistant system prompt is used - Assistant Tools are available - If provided: - Fully replaces the default system prompt - **Assistant Tools are disabled** Typical use cases: - Creating a highly customized chat role - Running the model without any tool access *** #### options.modelId (optional) - Type: `string` - Specifies the model to use for this conversation - Users may still change the model in the chat UI (if allowed) *** #### options.provider (optional) - Type: `Provider` - Specifies the default provider for the conversation - Users may change the provider in the chat UI (if allowed) *** ### Return Value ```ts Promise ``` - Resolves when the conversation is successfully created - Rejects if a conversation already exists *** ## present ### API Definition ```ts function present(): Promise ``` *** ### Behavior - Presents the Assistant chat page for the current conversation - If the page is already presented, calling this has no effect - Can be called: - After `startConversation` - After `dismiss` to re-present the same conversation *** ### Return Value ```ts Promise ``` - Resolves when the chat page is dismissed by the user *** ## dismiss ### API Definition ```ts function dismiss(): Promise ``` *** ### Behavior - Dismisses the Assistant chat page - **Does not stop the conversation** - Conversation state and history are preserved Typical use cases: - Temporarily hiding the chat UI - Navigating to another page or task *** ### Return Value ```ts Promise ``` *** ## stopConversation ### API Definition ```ts function stopConversation(): Promise ``` *** ### Behavior - Fully terminates the current conversation - Automatically calls `dismiss` - Cleans up conversation state and resources - After calling this, a new conversation may be started *** ### Return Value ```ts Promise ``` *** ## Conversation State Flags ### Assistant.isAvailable ```ts const isAvailable: boolean ``` - Indicates whether the current user has access to the Assistant - If `false`, all Conversation APIs are unavailable *** ### Assistant.isPresented ```ts const isPresented: boolean ``` - Indicates whether the Assistant chat page is currently presented *** ### Assistant.hasActiveConversation ```ts const hasActiveConversation: boolean ``` - Indicates whether there is an active conversation - Commonly used to guard against duplicate `startConversation` calls *** ## Examples ### Example 1: Typical usage ```ts await Assistant.startConversation({ message: "Help me summarize this article.", autoStart: true }) await Assistant.present() ``` *** ### Example 2: Create a conversation without auto-sending ```ts await Assistant.startConversation({ message: "Let's discuss system architecture design.", autoStart: false }) await Assistant.present() // User manually presses Send in the UI ``` *** ### Example 3: Dismiss and re-present the same conversation ```ts await Assistant.startConversation({ message: "Analyze this image.", images: [image], autoStart: true }) await Assistant.present() await Assistant.dismiss() // Later, re-present the same conversation await Assistant.present() ``` *** ### Example 4: Stop the current conversation and start a new one ```ts if (Assistant.hasActiveConversation) { await Assistant.stopConversation() } await Assistant.startConversation({ message: "Start a new topic.", autoStart: true }) await Assistant.present() ``` *** ## Best Practices - Treat Conversation APIs as a **managed chat UI** - Do not mix Conversation APIs with `requestStreaming` in the same flow - Always check `hasActiveConversation` before calling `startConversation` - For one-shot or data-oriented tasks, prefer: - `requestStructuredData` - `requestStreaming` - Use Conversation APIs when continuous user interaction is required *** ## Design Boundaries - Conversation APIs are not suitable for headless or background tasks - Not intended for fully automated workflows - Not ideal when you need strict control over prompts, tokens, or output format --- url: /guide/Assistant/Assistant Quick Start.md --- # Assistant Quick Start The Assistant API in Scripting provides three distinct capabilities, each designed for a different type of use case: **structured data**, **streaming output**, and **interactive conversations**. Before choosing an API, first decide **what kind of result you need**. *** ## Assistant API Overview | Category | Main APIs | Best For | | ---------------- | ---------------------------------------------------------------- | -------------------------------- | | Structured Data | `requestStructuredData` | Extracting predictable JSON data | | Streaming Output | `requestStreaming` | Real-time text generation | | Conversations | `startConversation` / `present` / `dismiss` / `stopConversation` | Fully managed chat UI | *** ## requestStructuredData **Purpose** Requests **strictly structured JSON output** that conforms to a provided schema. **Best suited for** - Parsing receipts, invoices, and bills - Extracting fields from natural language - Generating configuration or rule objects - Any output that must be consumed by program logic **Key characteristics** - Stable and predictable output - No streaming or incremental updates - Ideal for background or headless scenarios **In one sentence** > If you need **data**, use `requestStructuredData`. *** ## requestStreaming **Purpose** Requests **streaming output**, allowing you to receive content incrementally as the model generates it. **Best suited for** - Typing-effect UI - Long-form content generation - Low-latency user feedback **Key characteristics** - Emits text, reasoning, and usage chunks - Can be rendered progressively - Output is not guaranteed to be structured **In one sentence** > If you need **real-time output**, use `requestStreaming`. *** ## Conversation APIs **Related methods** - `startConversation` - `present` - `dismiss` - `stopConversation` **Purpose** Creates and presents a **system-hosted Assistant chat experience**. **Best suited for** - ChatGPT-style interactions - Multi-turn conversations - Scenarios where the system manages UI, streaming, and provider switching **Key characteristics** - Built-in chat UI - Streaming handled automatically - Only one active conversation at a time **In one sentence** > If you need a **full chat experience**, use the Conversation APIs. *** ## How to Choose the Right API ### Common Scenarios - **Parse a receipt →** `requestStructuredData` - **Show AI writing text live →** `requestStreaming` - **Open a chat interface for users →** Conversation APIs - **No UI, just results →** `requestStructuredData` or `requestStreaming` - **Let the system manage the chat UI →** Conversation APIs *** ## Minimal Examples ### Structured Data ```ts const result = await Assistant.requestStructuredData(...) ``` *** ### Streaming Output ```ts const stream = await Assistant.requestStreaming(...) for await (const chunk of stream) { // handle chunk } ``` *** ### Conversation ```ts await Assistant.startConversation({ message: "Hello", autoStart: true }) await Assistant.present() ``` *** ## Usage Tips - Do not mix Conversation APIs with `requestStreaming` in the same flow - Prefer `requestStructuredData` whenever output must be consumed as data - Use streaming or conversations for presentation-focused scenarios *** ## Next Steps For deeper details, refer to: - `requestStructuredData` – detailed schema-driven data extraction - `requestStreaming` – streaming behavior and chunk handling - Conversation APIs – lifecycle and interaction patterns --- url: /guide/Assistant/requestStreaming.md --- # requestStreaming `requestStreaming` requests a **streaming response** from the Assistant. Instead of returning a complete result at once, the Assistant emits **chunks incrementally** as the model generates output. This enables: - Real-time UI updates (typing effect) - Low-latency handling of long responses - Progressive rendering of results - Streaming logs and intermediate output handling The API returns a **`ReadableStream`**, which can be consumed using `for await ... of`. *** ## API Definition ```ts function requestStreaming(options: { systemPrompt?: string | null messages: MessageItem | MessageItem[] provider?: Provider modelId?: string }): Promise> ``` *** ## Parameters ### options.systemPrompt (optional) - Type: `string | null` - Specifies the system prompt for this request. - If omitted: - The default Assistant system prompt is used. - If provided: - It **fully replaces** the default system prompt. - Assistant Tools are **not available**. Typical use cases: - Defining a strict role (e.g. reviewer, translator, summarizer) - Enforcing output tone or behavior - Running the model without built-in tools *** ### options.messages - Type: `MessageItem | MessageItem[]` - Required - Represents the conversation context sent to the model. #### MessageItem ```ts type MessageItem = { role: "user" | "assistant" content: MessageContent | MessageContent[] } ``` - `role` - `"user"`: user input - `"assistant"`: previous assistant messages (for context) *** ### MessageContent Types #### Text ```ts type MessageTextContent = | string | { type: "text"; content: string } ``` *** #### Image ```ts type MessageImageContent = { type: "image" content: string // data:image/...;base64,... } ``` *** #### Document ```ts type MessageDocumentContent = { type: "document" content: { mediaType: string data: string // base64 } } ``` *** ### options.provider (optional) - Type: `Provider` - Specifies the AI provider. - If omitted, the currently configured default provider is used. - Supported values: - `"openai"` - `"gemini"` - `"anthropic"` - `"deepseek"` - `"openrouter"` - `{ custom: string }` *** ### options.modelId (optional) - Type: `string` - Specifies the model ID. - Must match a model actually supported by the selected provider. - If omitted, the provider’s default model is used. *** ## Return Value ```ts Promise> ``` Once resolved, you receive a stream that can be consumed asynchronously. *** ## StreamChunk Types The stream may emit the following chunk types. *** ### StreamTextChunk ```ts type StreamTextChunk = { type: "text" content: string } ``` - Represents user-visible generated text. - Multiple chunks concatenated form the final response. *** ### StreamReasoningChunk ```ts type StreamReasoningChunk = { type: "reasoning" content: string } ``` - Represents intermediate reasoning produced by the model. - Availability and granularity depend on the provider and model. *** ### StreamUsageChunk ```ts type StreamUsageChunk = { type: "usage" content: { totalCost: number | null cacheReadTokens: number | null cacheWriteTokens: number | null inputTokens: number outputTokens: number } } ``` Notes: - Typically emitted once near the end of the stream. - Some providers may omit certain fields. - `totalCost` may be `null` if the provider does not expose pricing data. *** ## Examples ### Example 1: Basic streaming request ```ts const stream = await Assistant.requestStreaming({ messages: { role: "user", content: "Tell me a short science fiction story." }, provider: "openai" }) let result = "" for await (const chunk of stream) { if (chunk.type === "text") { result += chunk.content console.log(chunk.content) } } ``` *** ### Example 2: Handling text, reasoning, and usage separately ```ts const stream = await Assistant.requestStreaming({ systemPrompt: "You are a precise technical writing assistant.", messages: [ { role: "user", content: "Explain what HTTP/3 is." } ] }) let answer = "" let reasoningLog = "" let usage = null for await (const chunk of stream) { switch (chunk.type) { case "text": answer += chunk.content break case "reasoning": reasoningLog += chunk.content break case "usage": usage = chunk.content break } } console.log(answer) console.log(usage) ``` *** ### Example 3: Streaming with document input ```ts const stream = await Assistant.requestStreaming({ messages: [ { role: "user", content: [ { type: "text", content: "Summarize the key points of this document." }, { type: "document", content: { mediaType: "application/pdf", data: "JVBERi0xLjQKJcfs..." } } ] } ], provider: "anthropic" }) for await (const chunk of stream) { if (chunk.type === "text") { console.log(chunk.content) } } ``` *** ## Usage Notes and Best Practices - Streams must be consumed **sequentially**; do not read concurrently. - For UI scenarios: - Render `text` chunks immediately. - Keep `reasoning` for debugging or developer modes. - Process `usage` after completion. - If you no longer need the output, stop consuming the stream to avoid unnecessary cost. - Not all providers/models emit `reasoning` or `usage`. - Do not assume a chunk represents a complete sentence; chunk sizes vary. --- url: /guide/Assistant/requestStructuredData.md --- # requestStructuredData `requestStructuredData` requests **structured JSON output** from the assistant that conforms to a provided JSON schema. This API is designed for workflows where you want a predictable, programmatically usable result rather than free-form text. Common use cases include: - Extracting structured fields from natural language - Parsing invoices, receipts, and tickets - Generating configuration objects - Normalizing data across different AI providers/models *** ## Supported JSON Schema Types Scripting defines a lightweight schema structure with three building blocks. ### Primitive ```ts type JSONSchemaPrimitive = { type: "string" | "number" | "boolean" required?: boolean description: string } ``` *** ### Object ```ts type JSONSchemaObject = { type: "object" properties: Record required?: boolean description: string } ``` *** ### Array ```ts type JSONSchemaArray = { type: "array" items: JSONSchemaType required?: boolean description: string } ``` *** ## API Signatures ### Without images ```ts function requestStructuredData( prompt: string, schema: JSONSchemaArray | JSONSchemaObject, options?: { provider: Provider modelId?: string } ): Promise ``` ### With images ```ts function requestStructuredData( prompt: string, images: string[], schema: JSONSchemaArray | JSONSchemaObject, options?: { provider: Provider modelId?: string } ): Promise ``` *** ## Parameters ### prompt - Type: `string` - Required - The instruction to the model describing what to extract or generate. - For best reliability, explicitly specify: - expected formats (e.g., ISO date) - currency rules - how to handle missing fields ### images (optional) - Type: `string[]` - Each item must be a **data URI**, e.g. `data:image/png;base64,...` - Not all providers/models support images. - Avoid passing too many images to reduce failure risk. ### schema - Type: `JSONSchemaArray | JSONSchemaObject` - Required - Defines the **only acceptable** JSON structure for the response. - Every field should have a clear `description` to guide the model. ### options.provider - Type: `Provider` - Optional (uses the default configured provider if omitted) - Supported: - `"openai" | "gemini" | "anthropic" | "deepseek" | "openrouter" | { custom: string }` ### options.modelId (optional) - Type: `string` - Must match a model actually supported by the chosen provider. - If omitted, Scripting uses the provider’s default model. *** ## Return Value ```ts Promise ``` - `R` is the generic type you provide. - The resolved value is expected to match your schema. - The promise rejects if the assistant cannot return a valid structured result. *** ## Examples ### Example 1: Parse a receipt/bill into line items (time + amount) This example asks the assistant to analyze a textual receipt and extract: - receipt time (`purchasedAt`) - line items (`items[]`) - item name - item time (if present; otherwise null) - amount - total amount ```ts type ReceiptItem = { name: string time: string | null amount: number } type ReceiptParsed = { purchasedAt: string | null currency: string | null items: ReceiptItem[] total: number | null } const receiptText = ` Star Coffee 2026-01-08 14:23 Latte (Large) $5.50 Blueberry Muffin $3.20 Tax $0.79 Total $9.49 ` const parsed = await Assistant.requestStructuredData( [ "Analyze the receipt text below and extract:", "- purchasedAt: the purchase date/time in ISO-8601 if possible", "- currency: currency code if you can infer it (otherwise null)", "- items: only actual purchasable items (exclude tax/total lines)", " - name: item name", " - time: item-level time if present, otherwise null", " - amount: numeric amount", "- total: numeric total if present, otherwise null", "", "Receipt:", receiptText ].join("\n"), { type: "object", description: "Parsed receipt content", properties: { purchasedAt: { type: "string", description: "Purchase date/time in ISO-8601 format if available, otherwise an empty string" }, currency: { type: "string", description: "Currency code like USD/EUR/CNY if inferable, otherwise an empty string" }, items: { type: "array", description: "Purchased line items (exclude tax/total/subtotal/service fee lines)", items: { type: "object", description: "A single purchased item line", properties: { name: { type: "string", description: "Item name" }, time: { type: "string", description: "Item-level time in ISO-8601 if available, otherwise an empty string" }, amount: { type: "number", description: "Item amount as a number" } } } }, total: { type: "number", description: "Total amount if present, otherwise -1" } } }, { provider: "openai" } ) // Post-processing suggestion: // Treat "" as null for purchasedAt/currency/time, and -1 as null for total. console.log(parsed) ``` *** ### Example 2: Generate an array ```ts type Expense = { name: string amount: number } const expenses = await Assistant.requestStructuredData( "List three common daily expenses with estimated amounts.", { type: "array", description: "A list of expenses", items: { type: "object", description: "A single expense item", properties: { name: { type: "string", description: "Expense name" }, amount: { type: "number", description: "Estimated amount" } } } }, { provider: "gemini" } ) ``` *** ### Example 3: Use images + schema ```ts type ImageSummary = { description: string containsText: boolean } const summary = await Assistant.requestStructuredData( "Analyze the image and summarize the main content.", ["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."], { type: "object", description: "Image analysis result", properties: { description: { type: "string", description: "What the image shows" }, containsText: { type: "boolean", description: "Whether readable text exists" } } }, { provider: "openai" } ) ``` *** ## Best Practices - Make the schema explicit and descriptive; ambiguous schemas lead to unstable results. - Prefer `requestStructuredData` over parsing free-form text when your output is used by program logic. - For business-critical extraction (e.g., finance/receipts), add strict formatting rules in `prompt`. --- url: /guide/Changelog/2.4.8/Intent.md --- # Intent Scripting allows you to define custom iOS Intents using an `intent.tsx` file. These scripts can receive input from the iOS share sheet or the Shortcuts app and return structured results. With optional UI presentation, you can create interactive workflows that process data and deliver output dynamically. *** ## 1. Creating and Configuring an Intent ### 1.1 Create an Intent Script 1. Create a new script project in the Scripting app. 2. Add a file named `intent.tsx` to the project. 3. Define your logic and optionally a UI component inside the file. ### 1.2 Configure Supported Input Types Tap the project title in the editor’s title bar to open **Intent Settings**, then select supported input types: - Text - Images - File URLs - URLs This configuration enables your script to appear in the share sheet or Shortcuts when matching input is provided. *** ## 2. Accessing Input Data Inside `intent.tsx`, use the `Intent` API to access input values. | Property | Description | | -------------------------- | ----------------------------------------------------------------------------------- | | `Intent.shortcutParameter` | A single parameter passed from the Shortcuts app, with `.type` and `.value` fields. | | `Intent.textsParameter` | Array of text strings. | | `Intent.urlsParameter` | Array of URL strings. | | `Intent.imagesParameter` | Array of image file paths (UIImage objects). | | `Intent.fileURLsParameter` | Array of local file URL paths. | Example: ```ts if (Intent.shortcutParameter) { if (Intent.shortcutParameter.type === "text") { console.log(Intent.shortcutParameter.value) } } ``` *** ## 3. Returning a Result Use `Script.exit(result)` to return a result to the caller, such as the Shortcuts app or another script. Valid return types include: - Plain text: `Intent.text(value)` - Attributed text: `Intent.attributedText(value)` - URL: `Intent.url(value)` - JSON: `Intent.json(value)` - File path or file URL: `Intent.file(value)` or `Intent.fileURL(value)` Example: ```ts import { Script, Intent } from "scripting" Script.exit(Intent.text("Done")) ``` *** ## 4. Displaying Interactive UI Use `Navigation.present()` to show a UI before returning a result. You can render a React-style component and then call `Script.exit()` after the interaction completes. Example: ```ts import { Intent, Script, Navigation, VStack, Text } from "scripting" function MyIntentView() { return ( {Intent.textsParameter?.[0]} ) } async function run() { await Navigation.present({ element: }) Script.exit() } run() ``` *** ## 5. Using Intents in the Share Sheet If a script supports a specific input type (e.g., text or image), it will automatically appear as an option in the iOS share sheet: 1. Select content such as text or a file. 2. Tap the Share button. 3. Choose **Scripting** in the share sheet. 4. Scripting will list scripts that support the selected input type. *** ## 6. Using Intents in the Shortcuts App You can call scripts from the Shortcuts app with or without UI: - **Run Script**: Executes the script in the background. - **Run Script in App**: Executes the script in the foreground, with UI presentation support. Steps: 1. Open the Shortcuts app and create a new shortcut. 2. Add the **Run Script** or **Run Script in App** action from Scripting. 3. Choose the target script and pass input parameters if needed. *** ## 7. Intent API Reference ### `Intent` Properties | Property | Type | Description | | ------------------- | ------------------- | ----------------------------------------------- | | `shortcutParameter` | `ShortcutParameter` | Input from Shortcuts with `.type` and `.value`. | | `textsParameter` | `string[]` | Array of input text values. | | `urlsParameter` | `string[]` | Array of input URLs. | | `imagesParameter` | `UIImage[]` | Array of image file paths or objects. | | `fileURLsParameter` | `string[]` | Array of input file paths (local file URLs). | ### `Intent` Methods | Method | Return Type | Example | | ------------------------------ | --------------------------- | -------------------------------------- | | `Intent.text(value)` | `IntentTextValue` | `Intent.text("Hello")` | | `Intent.attributedText(value)` | `IntentAttributedTextValue` | `Intent.attributedText("Styled Text")` | | `Intent.url(value)` | `IntentURLValue` | `Intent.url("https://example.com")` | | `Intent.json(value)` | `IntentJsonValue` | `Intent.json({ key: "value" })` | | `Intent.file(path)` | `IntentFileValue` | `Intent.file("/path/to/file.txt")` | | `Intent.fileURL(path)` | `IntentFileURLValue` | `Intent.fileURL("/path/to/file.pdf")` | | `Intent.image(UIImage)` | `IntentImageValue` | `Intent.image(uiImage)` | | `Intent.view(node, value?)` | `IntentViewValue` | `Intent.view()` | *** ## 8. Best Practices and Notes - Always call `Script.exit()` to properly terminate the script and return a result. - When displaying a UI, ensure `Navigation.present()` is awaited before calling `Script.exit()`. - Use **"Run Script in App"** for large files or images to avoid process termination due to memory constraints. - You can use `queryParameters` when launching scripts via URL scheme if additional data is needed. --- url: /guide/Changelog/2.4.8/LanguageModelSession.md --- # LanguageModelSession `LanguageModelSession` provides access to Apple Intelligence local language models through iOS Foundation Models. It allows scripts to perform on-device AI tasks such as text generation, structured JSON output, and streaming responses. This API encapsulates system-level language model capabilities and supports: - On-device AI inference (Apple Intelligence) - JSON Schema structured output - Streaming token responses - Session-level resource management - Controlled generation parameters `LanguageModelSession` is a global API and does not require importing from the `scripting` module. *** ## Availability ```ts LanguageModelSession.isAvailable ``` Indicates whether Foundation Models are available on the current device. Return value: ```ts boolean ``` Notes: - Only devices supporting Apple Intelligence will return `true`. - You should check availability before creating a session. Example: ```ts if (!LanguageModelSession.isAvailable) { console.log("LanguageModelSession is not available on this device") } ``` *** ## Creating a Session ```ts new LanguageModelSession(options?) ``` Parameters: ```ts { instructions?: string } ``` Description: - Creates a new language model session. - `instructions` act as system-level guidance controlling model behavior. - Instructions remain active throughout the session lifecycle. Example: ```ts const session = new LanguageModelSession({ instructions: "You are a professional financial assistant." }) ``` *** ## Properties ## isResponding ```ts readonly isResponding: boolean ``` Description: - Indicates whether the model is currently generating a response. - Useful for managing UI state. *** ## Prewarming the Model ```ts prewarm(promptPrefix?: string): void ``` Description: - Requests the system to load required model resources in advance. - Reduces initial response latency. - Optionally caches a prompt prefix. Parameters: ```ts promptPrefix?: string ``` Example: ```ts session.prewarm("You are a JSON extraction assistant") ``` Recommended use cases: - Before the user initiates an AI task - When reducing first-token latency is important *** ## Generating Responses ```ts respond(prompt, options?) ``` Returns: ```ts Promise<{ content: string json: T | null }> ``` Description: - Produces a complete response. - Supports structured JSON output via schema validation. - Automatically attempts JSON parsing. Parameters: ```ts { temperature?: number maxResponseTokens?: number schema?: JSONSchemaObject } ``` Parameter details: - `temperature` - Range: 0.0 \~ 1.0 - Higher values increase randomness. - `maxResponseTokens` - Maximum number of tokens to generate. - `schema` - Expected JSON structure. - If generated content is not valid JSON, `json` will be `null`. *** ## Structured Output Example ```ts const session = new LanguageModelSession() const result = await session.respond( `Parse the following receipt: Parking fee Convention Center 2026-02-09 12:23:45 ¥19 Credit card payment`, { schema: { type: "object", description: "Receipt JSON", properties: { amount: { type: "number", description: "Amount" }, category: { type: "string", description: "Category" }, address: { type: "string", description: "Address", required: false }, isIncome: { type: "boolean", decription: "Whether it is an income" } } } } ) console.log(result.content) console.log(result.json) ``` *** ## Streaming Responses ```ts streamResponse(prompt, options?) ``` Returns: ```ts Promise ``` Description: - Produces streaming output. - Suitable for chat interfaces or incremental display. Parameters: ```ts { temperature?: number maxResponseTokens?: number } ``` Example: ```ts const stream = await session.streamResponse( "Tell a joke about a dog", { temperature: 0.7 } ) let fullText = "" for await (const chunk of stream) { console.log(chunk) fullText = chunk } ``` *** ## Releasing Resources ```ts dispose(): void ``` Description: - Releases resources associated with the session. - Recommended after finishing usage to reduce memory usage. Example: ```ts session.dispose() ``` *** ## Complete Example ```tsx import { Script } from "scripting" console.present().then(() => { Script.exit() }) async function run() { if (!LanguageModelSession.isAvailable) { console.log("Not supported on this device") return } const session = new LanguageModelSession({ instructions: "You are a structured data extraction assistant." }) session.prewarm("Parse receipt") const result = await session.respond( "Parse: Parking fee Convention Center ¥19", { temperature: 0.2, schema: { type: "object", description: "JSON result", properties: { amount: { type: "number", description: "Amount" }, category: { type: "string", description: "Category" } } } } ) console.log(result.json) const stream = await session.streamResponse( "Tell a joke about a dog" ) for await (const text of stream) { console.log(text) } session.dispose() } run() ``` *** ## Best Practices - Reuse a session for multiple requests when possible. - Call `prewarm` before first use to reduce latency. - Use `schema` for reliable structured output. - Use `streamResponse` for long-form or interactive responses. - Call `dispose` when finished to release resources. --- url: /guide/Changelog/2.4.8/Reminder.md --- # Reminder The `Reminder` API provides the ability to create, edit, and manage reminders in the iOS calendar system. It supports configuring due dates through `DateComponents`, assigning priorities, adding notes, managing recurrence rules, working with alarms, and tracking completion state. This API is suitable for a wide range of task and schedule reminder scenarios. *** ## 1. Class: `Reminder` The `Reminder` class represents an individual reminder item and provides properties and methods to read and modify its data. *** ## 2. Properties ### identifier: string A unique identifier assigned by the system (read-only). ### calendar: Calendar | null The calendar to which the reminder belongs. The calendar can be null if the reminder is not associated with a calendarm, but you must do not set the calendar to null. ### title: string The title or summary of the reminder. ### notes: string | null Optional notes providing additional context. *** ## Completion State ### isCompleted: boolean Indicates whether the reminder is marked as completed. - Setting this property to `true` automatically sets `completionDate` to the current date. - Setting it to `false` sets `completionDate` to `null`. Special consideration: If a reminder is completed on another device or client, `isCompleted` may be `true` while `completionDate` remains `null`. ### completionDate: Date | null The date on which the reminder was completed. - Assigning a date sets `isCompleted = true`. - Assigning `null` clears the completed state. *** ## Due Date ### dueDateComponents: DateComponents | null Represents the reminder’s due date using date components. Supports partially specified date or time fields. Useful for date-based or recurring reminders. You may use `DateComponents.isValidDate` to check whether the components form a valid date. ### dueDate: Date | null Deprecated. Use `dueDateComponents` instead. You can read the equivalent date using `dueDateComponents?.date`. ### dueDateIncludesTime: boolean Deprecated. Use `dueDateComponents?.hour != null && dueDateComponents?.minute != null` to determine whether the due date includes a time component. *** ## Priority ### priority: number An integer representing the reminder’s priority. Higher values typically indicate greater importance or urgency. *** ## Recurrence ### recurrenceRules: RecurrenceRule\[] | null An array of recurrence rules associated with the reminder. ### hasRecurrenceRules: boolean Indicates whether the reminder contains recurrence rules (read-only). *** ## Alarms ### alarms: EventAlarm\[] | null A collection of alarms associated with the reminder. Alarms may be based on: - absolute dates - relative offsets - structured locations (geofence triggers) ### hasAlarm: boolean Indicates whether the reminder contains any alarms. *** ## Attendees ### attendees: EventParticipant\[] | null An array of attendee objects (read-only). Not all reminder sources support attendee data. ### hasAttendees: boolean Indicates whether the reminder has attendees. *** ## State Indicators ### hasNotes: boolean Indicates whether the reminder contains notes. ### hasChanges: boolean Indicates whether the reminder or any of its nested objects contains unsaved changes. *** ## 3. Instance Methods ### addAlarm(alarm: EventAlarm): void Adds an alarm to the reminder. ### removAlarm(alarm: EventAlarm): void Removes the specified alarm. (Method name is `removAlarm`.) *** ### addRecurrenceRule(rule: RecurrenceRule): void Adds a recurrence rule. ### removeRecurrenceRule(rule: RecurrenceRule): void Removes a recurrence rule. *** ### `save(): Promise` Saves changes to the reminder. If the reminder has not been saved before, it is added to its associated calendar. ### `remove(): Promise` Deletes the reminder from the calendar. *** ## 4. Static Methods ### `Reminder.get(identifier: string): Promise` Returns a reminder by its identifier, or `null` if not found. *** ### `Reminder.getAll(calendars?: Calendar[]): Promise` Returns all reminders, optionally filtered by the specified calendars. *** ### `Reminder.getIncompletes(options?): Promise` Returns incomplete reminders filtered by due date range and/or calendar set. Options: - `startDate?: Date` Includes reminders whose due date is after this date. - `endDate?: Date` Includes reminders whose due date is before this date. - `calendars?: Calendar[]` Specifies which calendars to search. This method does not expand recurrence rules; it only returns reminders with concrete due dates. *** ### `Reminder.getCompleteds(options?): Promise` Returns completed reminders filtered by completion date range and/or calendar set. Options: - `startDate?: Date` Includes reminders completed after this date. - `endDate?: Date` Includes reminders completed before this date. - `calendars?: Calendar[]` Specifies which calendars to search. *** ## 5. Usage Examples ## Creating a Reminder with DateComponents ```ts const reminder = new Reminder() reminder.title = "Prepare meeting materials" reminder.notes = "Finish before Monday’s team meeting" reminder.dueDateComponents = new DateComponents({ year: 2025, month: 10, day: 6, hour: 9, minute: 30, }) reminder.priority = 2 await reminder.save() ``` *** ## Creating a Date-Only Reminder ```ts reminder.dueDateComponents = new DateComponents({ year: 2025, month: 10, day: 6, }) ``` *** ## Creating DateComponents from a Date ```ts const now = new Date() reminder.dueDateComponents = DateComponents.fromDate(now) ``` *** ## Fetching All Reminders ```ts const reminders = await Reminder.getAll() for (const r of reminders) { console.log(`Reminder: ${r.title}`) } ``` *** ## Fetching Incomplete Reminders ```ts const incompletes = await Reminder.getIncompletes({ startDate: new Date("2025-01-01"), endDate: new Date("2025-01-31"), }) ``` *** ## Marking a Reminder as Completed ```ts reminder.isCompleted = true await reminder.save() ``` *** ## Deleting a Reminder ```ts await reminder.remove() ``` *** ## 6. Additional Notes ### Date Management Using `dueDateComponents` is recommended for all due-date handling. It supports: - date-only values - date with time - partial components - validity checks through `isValidDate` ### Recurrence Reminder queries do not expand recurrence rules. They operate only on the reminder objects that have concrete due dates. Recurrence rules can be added or removed through the API. ### Alarms Alarms may be absolute, relative, or location-based, and are shared with the `CalendarEvent` API. ### Attendees Some reminder sources do not support attendee data; in such cases, the attendees array may be `null`. --- url: /guide/Changelog/2.4.8/Rich Text.md --- # Rich Text The `Text` component supports rendering rich text using the `styledText` property, allowing fine-grained control beyond plain text or Markdown rendering. With `StyledText`, developers can: - Apply detailed text styling such as fonts, colors, strokes, and decorations - Build nested rich text structures - Add tappable links or gesture handlers - Control paragraph-level layout and typography The newly introduced `paragraphStyle` field provides advanced paragraph layout control. It maps conceptually to native paragraph layout systems (similar to `NSParagraphStyle`) and enables: - Text alignment control - Line spacing and paragraph spacing - First-line indentation - Truncation strategies - Automatic hyphenation - Multi-language writing direction support *** ## StyledText ### Type Definition ```ts type StyledText = { font? fontDesign? fontWeight? italic? bold? baselineOffset? kerning? monospaced? monospacedDigit? underlineColor? underlineStyle? strokeColor? strokeWidth? strikethroughColor? strikethroughStyle? foregroundColor? backgroundColor? paragraphStyle?: ParagraphStyle content: string | (string | StyledText)[] link?: string onTapGesture?: () => void } ``` *** ### content Defines the text content. Can be: - A string - An array of strings and nested `StyledText` objects Example: ```tsx ``` *** ### Font-related Properties | Property | Description | | --------------- | -------------------- | | font | Font style | | fontDesign | Font design variant | | fontWeight | Font weight | | italic | Applies italic style | | bold | Applies bold style | | baselineOffset | Baseline offset | | kerning | Character spacing | | monospaced | Monospaced font | | monospacedDigit | Monospaced digits | *** ### Decoration Properties #### Underline ```ts underlineColor underlineStyle ``` Supported `UnderlineStyle` values: - single - double - thick - byWord - patternDash - patternDashDot - patternDashDotDot - patternDot *** #### Strikethrough ```ts strikethroughColor strikethroughStyle ``` *** #### Stroke (Outline) ```ts strokeColor strokeWidth ``` *** ### Color Properties ```ts foregroundColor backgroundColor ``` *** ### Interaction Properties #### link Defines a URL link. #### onTapGesture Tap gesture callback. *** ## ParagraphStyle ### Overview `paragraphStyle` controls paragraph-level typography and layout behavior. Recommended for: - Multi-line text layouts - Reading interfaces - Rich typography layouts - Multi-language content *** ### alignment Text alignment. ```ts alignment?: "left" | "center" | "right" | "justified" | "natural" ``` - left: left-aligned - center: centered - right: right-aligned - justified: fully justified - natural: system default *** ### firstLineHeadIndent Indentation applied only to the first line. ```ts firstLineHeadIndent?: number ``` Example: ```tsx paragraphStyle: { firstLineHeadIndent: 20 } ``` *** ### headIndent Left indentation applied to lines except the first. *** ### tailIndent Right-side indentation. Note: - Positive values are measured from the left - Negative values are measured from the right *** ### paragraphSpacing Additional spacing between paragraphs. *** ### lineSpacing Line spacing between lines. *** ### lineBreakMode Text truncation and wrapping strategy. ```ts lineBreakMode?: | "byCharWrapping" | "byClipping" | "byTruncatingHead" | "byTruncatingTail" | "byTruncatingMiddle" ``` Descriptions: - byCharWrapping: wrap by character - byClipping: clip overflow - byTruncatingHead: truncate at head - byTruncatingTail: truncate at tail - byTruncatingMiddle: truncate in middle *** ### minLineHeight / maxLineHeight Control minimum and maximum line height. *** ### lineHeightMultiple Multiplier applied to the natural line height. Example: ```ts lineHeightMultiple: 1.5 ``` *** ### baseWritingDirection Writing direction. ```ts baseWritingDirection?: "natural" | "leftToRight" | "rightToLeft" ``` Useful for: - RTL language support - Mixed-direction text *** ### hyphenationFactor Hyphenation level (0–1). Higher values increase likelihood of word splitting. *** ### usesDefaultHyphenation Whether to use system default hyphenation behavior. *** ## Full Example ```tsx ``` *** ## Usage Recommendations ### When to Use paragraphStyle Recommended scenarios: - Long-form reading interfaces - Article typography - Chat bubble layout improvements - Multi-language typesetting - Custom text layout *** ### Choosing Between styledText and Markdown | Scenario | Recommendation | | ------------------------------ | --------------------------- | | Simple formatting | Markdown | | Highly customizable styling | styledText | | Paragraph-level layout control | styledText + paragraphStyle | *** ### Performance Tips - Avoid excessively deep nesting - Reuse style objects when possible - For long content, consider splitting into segments --- url: /guide/Changelog/2.4.8/Slider/index.md --- # Slider The `Slider` component allows users to select a value from a bounded linear range of values. You can configure the slider by setting the minimum value, maximum value, step size, and the current value. The component also supports custom labels to describe the minimum and maximum values, as well as the slider itself. Additionally, it provides callbacks for handling value changes and editing state changes. ## SliderProps Type `SliderProps` defines the properties for the `Slider` component, which includes the following fields: - **min** (`number`): - The minimum value of the slider. - **Required**. - **max** (`number`): - The maximum value of the slider. - **Required**. - **step** (`number`): - The step size between each valid value of the slider. - **Optional**, defaults to `1`. - **value** (`number`): - The selected value within bounds. - **Required**, must be between `min` and `max`. - **onChanged** (`(value: number) => void`): - A callback function that is called whenever the slider value changes. - **Required**, called each time the value is updated. - **onEditingChanged** (`(value: boolean) => void`): - An optional callback function called when the editing state of the slider changes. - `value` is `true` when editing starts, and `false` when editing ends. - **label** (`VirtualNode`): - An optional view that describes the purpose of the slider. Even if some slider styles do not display the label, the system uses it for accessibility purposes (e.g., VoiceOver). - **Optional**. - **minValueLabel** (`VirtualNode`): - An optional view that describes the minimum value of the slider. - **Optional**, used only in the `SliderWithRangeValueLabelProps` mode. - **maxValueLabel** (`VirtualNode`): - An optional view that describes the maximum value of the slider. - **Optional**, used only in the `SliderWithRangeValueLabelProps` mode. ## SliderWithRangeValueLabelProps Type `SliderWithRangeValueLabelProps` is a type that defines additional properties for labeling the slider's range. It includes: - **label** (`VirtualNode`): - The label that describes the purpose of the slider. - **minValueLabel** (`VirtualNode`): - The label that describes the minimum value of the slider. - **maxValueLabel** (`VirtualNode`): - The label that describes the maximum value of the slider. ## Other Properties - **sliderThumbVisibility** (`Visibility`): - Sets the visibility of the slider's thumb. - iOS 26+ only. ## Usage Example Here’s an example of using the `Slider` component: ```tsx import { Slider } from 'scripting' const ExampleSlider = () => { const [value, setValue] = useState(50) const handleChange = (newValue: number) => { setValue(newValue) } return ( Adjust Volume} minValueLabel={0} maxValueLabel={100} /> ) } ``` In this example, the `Slider` component is configured with a range from `0` to `100`, with the default value set to `50`. Labels for the slider's purpose, as well as the minimum and maximum values, are provided. ## Important Notes - The `min` and `max` properties of the `Slider` must be numeric, and the `value` must be within this range. - The `onChanged` callback will trigger whenever the slider's value changes, passing the new value. - If you use `SliderWithRangeValueLabelProps`, you must provide appropriate view elements for `minValueLabel` and `maxValueLabel`. ## Summary The `Slider` component is a versatile UI control suitable for scenarios where the user needs to select a numerical value. With its flexible properties and callbacks, it can handle a wide range of use cases, especially when labels for the minimum, maximum values, or the slider itself are needed. --- url: /guide/Changelog/2.4.8/Slider/index_example.md --- # Example ```tsx import { useState, Slider, Text, VStack, NavigationStack, Navigation, Script } from "scripting" function Example() { const [value, setValue] = useState(15) return {value}} minValueLabel={0} maxValueLabel={100} /> Current value: {value} } async function run() { await Navigation.present({ element: }) Script.exit() } run() ``` --- url: /guide/Changelog/2.4.9/Assistant Tool With Interactive UI.md --- # Assistant Tool With Interactive UI AssistantTool supports interactive UI tools that render custom in-tool interfaces and return structured results after user interaction. This mode is useful when the Assistant needs user decisions that cannot be expressed reliably as plain text, such as option selection, confirmation details, staged forms, or visual previews. *** ## 1. Configuration in `assistant_tool.json` To enable interactive UI rendering: ```json { "displayName": "Select Options", "id": "select_options", "description": "Ask user to select one or more options.", "icon": "checklist", "color": "systemTeal", "parameters": [], "requireApproval": false, "autoApprove": true, "scriptEditorOnly": false, "supportsUIRender": true, "keepRenderingAfterComplete": false } ``` ### Key fields - `supportsUIRender: true` The tool is expected to provide a UI renderer via `AssistantTool.registerUIView(...)`. - `keepRenderingAfterComplete` Controls whether the UI should remain rendered after `response(...)` returns the result. *** ## 2. Registration API Interactive UI tools register a function component: ```ts AssistantTool.registerUIView

(view) ``` Where `view` receives: ```ts type UIProps

= { params: P response: (result: AssistantToolResponseResult) => void isAutoApprove: boolean scriptEditorProvider?: ScriptEditorProvider } ``` The component should render synchronously and return a `VirtualNode`. *** ## 3. Returning Results from UI Use `response(result)` when the user completes interaction. ```ts type AssistantToolResponseResult = { success: boolean output: { userParts?: AssistantToolOutputPart[] assistantParts?: AssistantToolOutputPart[] } } ``` Notes: - `response` can be called only once; later calls are ignored. - `userParts` are shown to users. - `assistantParts` are sent back to the Assistant for reasoning. *** ## 4. State Management During UI Interaction Interactive UI tools can persist state per tool call: ```ts AssistantTool.getState(key) AssistantTool.setState(key, value) AssistantTool.removeState(key) AssistantTool.clearState() ``` State is JSON-serializable and persisted with tool-call history. Typical uses: - selected options - current step index - temporary form values - partial progress flags *** ## 5. Relationship with Approval If `requireApproval: true` and `supportsUIRender: true`: 1. Approval request flow runs first 2. If approved, UI renderer is invoked 3. If rejected, renderer is not invoked This keeps sensitive operations controllable while still allowing rich interaction after consent. *** ## 6. Auto-Approve Behavior `isAutoApprove` is passed to `UIProps`. Use it when your tool can safely skip interaction in specific cases and return immediately: ```ts if (isAutoApprove && canUseDefaultResult) { response({ success: true, output: { userParts: [{ type: "text", text: "Default option selected." }], assistantParts: [{ type: "text", text: "Used default selection in auto-approve mode." }] } }) } ``` *** ## 7. Minimal Example ```ts type Params = { title: string; options: string[] } function ToolView({ params, response }: AssistantTool.UIProps) { const [selected, setSelected] = useState( AssistantTool.getState("selected") ) const choose = (value: string) => { setSelected(value) AssistantTool.setState("selected", value) } return ( {params.title} {params.options.map(option => ( )} ``` These actions call the native `AlarmManager` operations for the alarm: - `pause` - `resume` - `stop` - `secondary`, when the alert secondary button uses `secondaryButtonBehavior: "countdown"` If you need extra script-specific behavior, register your own `AppIntent` separately and use it in a normal Live Activity button. The standard `actions` field is the recommended way to control the alarm itself. *** ## Notes - `AlarmManager` alert `stopIntent` and `secondaryIntent` still belong to the system alert presentation. - Custom Live Activity buttons should use `state.actions.*.intent` for alarm pause, resume, stop, and countdown behavior. - Keep Dynamic Island compact regions small. Prefer SF Symbols, short labels, and system fonts. - Prefer `buttonStyle="glass"` or `buttonStyle="glassProminent"` on iOS 26 when the control is visible in the Lock Screen or expanded Dynamic Island. --- url: /guide/Changelog/3.0.0/Alarm/AlarmManager.md --- # AlarmManager `AlarmManager` (iOS 26+) is a global API provided by Scripting for creating and managing AlarmKit-based alarms, timers, and countdown reminders. It does not need to be imported and can be used directly in scripts. This API is suitable for scenarios such as: - Creating one-time alarms, such as “remind me tonight at 10:30 PM” - Creating daily or weekly recurring alarms - Creating Pomodoro timers, workout timers, and countdown-based reminders - Customizing alert titles, buttons, colors, icons, and metadata - Using `AppIntent` to handle alert button actions - Listening for alarm state changes and updating UI accordingly *** ## Availability Before using `AlarmManager`, you should first check whether the current environment supports AlarmKit: ```tsx if (!AlarmManager.isAvailable) { console.log("AlarmKit is not available on this device") } ``` If the current device or system version does not support it, related methods may not work. *** ## Core Concepts `AlarmManager` is built around these main types: - `Alarm`: an existing alarm instance - `Schedule`: the schedule rule that determines when an alarm fires - `Countdown`: countdown-related parameters - `Button`: button styling for the alert UI - `Sound`: alert sound configuration - `AlertPresentation`: content shown when the alarm is alerting - `CountdownPresentation`: content shown while countdown is running - `PausedPresentation`: content shown while countdown is paused - `Attributes`: the overall presentation configuration - `Configuration`: the final configuration object used to create alarms, timers, or countdown reminders *** ## Alarm ID Rules The `id` passed to an alarm must be generated with: ```tsx UUID.string() ``` Do not manually build fixed IDs, and do not reuse old IDs. Every new alarm, timer, or countdown reminder should use a newly generated UUID string. Correct example: ```tsx const id = UUID.string() ``` Not recommended: ```tsx const id = "morning-alarm" const id = `timer-${Date.now()}` ``` All later operations such as `cancel`, `stop`, `pause`, `resume`, and `startCountdown` must use the exact same ID that was used when the alarm was created. *** ## AlarmState ```tsx type AlarmState = "scheduled" | "countdown" | "paused" | "alerting" ``` Represents the current state of an alarm. ### `"scheduled"` The alarm has been scheduled and is waiting to fire. Common cases: - Fixed-time alarms - Weekly recurring alarms - Countdown-based alarms that have not started yet ### `"countdown"` A countdown is currently running. Common cases: - An active timer - A started countdown reminder ### `"paused"` The countdown is currently paused. ### `"alerting"` The alarm has fired and is currently presenting its alert. *** ## SecondaryButtonBehavior ```tsx type SecondaryButtonBehavior = "countdown" | "custom" ``` Controls the behavior of the secondary button shown in the alert UI. ### `"countdown"` The secondary button starts a countdown flow. This is typically used for “remind me later” or “remind me again in a few minutes” behavior. ### `"custom"` The secondary button is treated as a custom action button, and its actual behavior is defined by `secondaryIntent`. *** ## AlarmAppIntent ```tsx type AlarmAppIntent = AppIntent ``` Represents an `AppIntent` that can be attached to alarm buttons. It is mainly used for: - Running additional logic when the stop button is tapped - Running custom logic when the secondary button is tapped - Integrating alarm interactions with Live Activity, Widget, or ControlWidget logic It can be assigned through: - `Configuration.alarm(...).stopIntent` - `Configuration.alarm(...).secondaryIntent` - `Configuration.timer(...).stopIntent` - `Configuration.timer(...).secondaryIntent` - `Configuration.countdown(...).stopIntent` - `Configuration.countdown(...).secondaryIntent` Note that the required protocol type here is: ```tsx AppIntentProtocol.LiveActivityIntent ``` That means any `AppIntent` used with `AlarmManager` should be registered with the `LiveActivityIntent` protocol. A full example is provided later in this document. *** ## AlarmUpdateListener ```tsx type AlarmUpdateListener = (alarms: Alarm[]) => void ``` A callback used to listen for alarm list updates. When alarms are created, cancelled, paused, resumed, started, or otherwise updated, the listener receives the latest `Alarm[]`. *** ## Alarm ```tsx class Alarm { readonly id: string readonly state: AlarmState readonly schedule?: Schedule | null readonly countdownDuration?: Countdown | null } ``` Represents an existing alarm object. ### `id` ```tsx readonly id: string ``` The unique identifier of the alarm. It must be generated with `UUID.string()` when the alarm is created. All later operations such as cancel, stop, pause, and resume depend on this value. ### `state` ```tsx readonly state: AlarmState ``` The current alarm state. ### `schedule` ```tsx readonly schedule?: Schedule | null ``` The scheduling rule for this alarm. Pure timers may not have a `schedule`. ### `countdownDuration` ```tsx readonly countdownDuration?: Countdown | null ``` The countdown-related configuration for this alarm. *** ## Schedule ```tsx class Schedule { readonly type: "fixed" | "relative" readonly date?: Date | null readonly hour?: number | null readonly minute?: number | null readonly weekdays?: number[] | null static fixed(date: Date): Schedule static relative(hour: number, minute: number): Schedule static weekly(hour: number, minute: number, weekdays: number[]): Schedule } ``` Describes when an alarm should fire. ### `type` ```tsx readonly type: "fixed" | "relative" ``` The schedule type. - `"fixed"`: a fixed absolute time - `"relative"`: a schedule based on hour and minute ### `date` ```tsx readonly date?: Date | null ``` The exact trigger date for a fixed-time schedule. ### `hour` ```tsx readonly hour?: number | null ``` The hour component of the schedule. ### `minute` ```tsx readonly minute?: number | null ``` The minute component of the schedule. ### `weekdays` ```tsx readonly weekdays?: number[] | null ``` The weekday array for weekly recurring schedules. The exact weekday number mapping depends on your underlying implementation. Typically this uses values from 1 to 7. ### `Schedule.fixed(date)` ```tsx static fixed(date: Date): Schedule ``` Creates a fixed-time schedule. ```tsx const schedule = AlarmManager.Schedule.fixed( new Date("2026-03-20T22:30:00") ) ``` ### `Schedule.relative(hour, minute)` ```tsx static relative(hour: number, minute: number): Schedule ``` Creates a schedule based on hour and minute. This is typically suitable for “every day at a given time” scenarios. ```tsx const schedule = AlarmManager.Schedule.relative(7, 30) ``` ### `Schedule.weekly(hour, minute, weekdays)` ```tsx static weekly(hour: number, minute: number, weekdays: number[]): Schedule ``` Creates a weekly recurring schedule. ```tsx const schedule = AlarmManager.Schedule.weekly(8, 0, [2, 3, 4, 5, 6]) ``` *** ## Countdown ```tsx class Countdown { readonly preAlert?: number | null readonly postAlert?: number | null static create(options?: { preAlert?: DurationInSeconds | null postAlert?: DurationInSeconds | null }): Countdown } ``` Defines countdown-related parameters. ### `preAlert` ```tsx readonly preAlert?: number | null ``` The countdown duration before the alert, in seconds. ### `postAlert` ```tsx readonly postAlert?: number | null ``` The additional duration after the alert, in seconds. The exact behavior depends on your underlying implementation. ### `Countdown.create(options?)` ```tsx static create(options?: { preAlert?: DurationInSeconds | null postAlert?: DurationInSeconds | null }): Countdown ``` Creates a countdown configuration object. ```tsx const countdown = AlarmManager.Countdown.create({ preAlert: 10 * 60, postAlert: 5 * 60 }) ``` *** ## Button ```tsx class Button { static create(options: { title?: string textColor?: Color systemImageName?: string }): Button } ``` Defines the appearance of a button shown in the alert UI. ### Parameters #### `title` The button title. #### `textColor` The button text color. In this documentation, `Color` should be provided as a hex string, for example: ```tsx "#ffffff" "#ff9500" "#34c759" ``` #### `systemImageName` The SF Symbols name to use for the button icon. ### `Button.create(options)` ```tsx static create(options: { title?: string textColor?: Color systemImageName?: string }): Button ``` ```tsx const stopButton = AlarmManager.Button.create({ title: "Stop", textColor: "#ffffff", systemImageName: "stop.fill" }) const snoozeButton = AlarmManager.Button.create({ title: "Later", textColor: "#ffffff", systemImageName: "timer" }) ``` *** ## Sound ```tsx class Sound { static default(): Sound static named(name: string): Sound } ``` Defines the alert sound. ### `Sound.default()` ```tsx static default(): Sound ``` Uses the system default sound. ```tsx const sound = AlarmManager.Sound.default() ``` ### `Sound.named(name)` ```tsx static named(name: string): Sound ``` Uses a sound with the specified name. ```tsx const sound = AlarmManager.Sound.named("bell") ``` Available names depend on your underlying implementation and system support. *** ## AlertPresentation ```tsx class AlertPresentation { static create(options: { title: string stopButton?: Button | null secondaryButton?: Button | null secondaryBehavior?: SecondaryButtonBehavior | null }): AlertPresentation } ``` Defines the UI shown when the alarm is alerting. ### Parameters #### `title` The alert title. This is required. #### `stopButton` > **Deprecated.** Starting with iOS 26.1 the alert's stop action is rendered by the system as a slider, not as a custom button. `stopButton` is kept only for backward compatibility and is silently ignored on iOS 26.1+. You can omit it. The stop button appearance (iOS 26.0 fallback only). #### `secondaryButton` The secondary button appearance. #### `secondaryBehavior` The behavior of the secondary button. Possible values: - `"countdown"` - `"custom"` ### `AlertPresentation.create(options)` ```tsx static create(options: { title: string stopButton?: Button | null secondaryButton?: Button | null secondaryBehavior?: SecondaryButtonBehavior | null }): AlertPresentation ``` ```tsx const alert = AlarmManager.AlertPresentation.create({ title: "Wake up", stopButton: AlarmManager.Button.create({ title: "Dismiss", textColor: "#ffffff", systemImageName: "xmark" }), secondaryButton: AlarmManager.Button.create({ title: "Remind me later", textColor: "#ffffff", systemImageName: "timer" }), secondaryBehavior: "countdown" }) ``` *** ## CountdownPresentation ```tsx class CountdownPresentation { static create(title?: string | null, pauseButton?: Button | null): CountdownPresentation } ``` Defines the UI shown while a countdown is running. ### `CountdownPresentation.create(title?, pauseButton?)` ```tsx static create(title?: string | null, pauseButton?: Button | null): CountdownPresentation ``` ```tsx const countdownPresentation = AlarmManager.CountdownPresentation.create( "Focus session in progress", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ) ``` *** ## PausedPresentation ```tsx class PausedPresentation { static create(title?: string | null, resumeButton?: Button | null): PausedPresentation | null } ``` Defines the UI shown while a countdown is paused. ### `PausedPresentation.create(title?, resumeButton?)` ```tsx static create(title?: string | null, resumeButton?: Button | null): PausedPresentation | null ``` ```tsx const pausedPresentation = AlarmManager.PausedPresentation.create( "Paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ) ``` *** ## Attributes ```tsx class Attributes { static create(options: { alert: AlertPresentation countdown?: CountdownPresentation | null paused?: PausedPresentation | null tintColor?: Color metadata?: Record liveActivity?: { name: string } }): Attributes | null } ``` Combines the full presentation configuration for an alarm. ### Parameters #### `alert` The alert presentation shown when the alarm fires. This is required. #### `countdown` The presentation shown while countdown is running. #### `paused` The presentation shown while countdown is paused. #### `tintColor` The main tint color. Hex strings are recommended, such as: ```tsx "#ffffff" "#007aff" "#ff9500" ``` #### `metadata` Additional metadata for your own business logic. #### `liveActivity` Optional custom Live Activity UI binding. If the current script provides `alarm_live_activity.tsx` and registers the same name with `AlarmLiveActivity.register`, Scripting renders that UI on the Lock Screen and Dynamic Island. Otherwise, it uses the built-in alarm UI. ```tsx liveActivity: { name: "FocusTimerActivity" } ``` See the **Alarm Live Activity** document for the full UI builder API. ### `Attributes.create(options)` ```tsx static create(options: { alert: AlertPresentation countdown?: CountdownPresentation | null paused?: PausedPresentation | null tintColor?: Color metadata?: Record liveActivity?: { name: string } }): Attributes | null ``` ```tsx const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Pomodoro finished", stopButton: AlarmManager.Button.create({ title: "Done", textColor: "#ffffff", systemImageName: "checkmark" }) }), countdown: AlarmManager.CountdownPresentation.create( "Focusing", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#ff9500", metadata: { type: "pomodoro", source: "study-script" }, liveActivity: { name: "FocusTimerActivity" } }) ``` *** ## Configuration ```tsx class Configuration { static alarm(options: { schedule?: Schedule | null attributes: Attributes sound?: Sound | null stopIntent?: AlarmAppIntent | null secondaryIntent?: AlarmAppIntent | null }): Configuration | null static timer(options: { duration: DurationInSeconds attributes: Attributes sound?: Sound | null stopIntent?: AlarmAppIntent | null secondaryIntent?: AlarmAppIntent | null }): Configuration | null static countdown(options: { countdown?: Countdown | null schedule?: Schedule | null attributes: Attributes sound?: Sound | null stopIntent?: AlarmAppIntent | null secondaryIntent?: AlarmAppIntent | null }): Configuration | null } ``` Creates the final configuration object passed to `AlarmManager.schedule()`. *** ## Configuration.alarm ```tsx static alarm(options: { schedule?: Schedule | null attributes: Attributes sound?: Sound | null stopIntent?: AlarmAppIntent | null secondaryIntent?: AlarmAppIntent | null }): Configuration | null ``` Creates a regular alarm configuration. Suitable for: - One-time alarms - Daily alarms - Weekly recurring alarms ### Parameters #### `schedule` The alarm schedule. #### `attributes` The presentation attributes. Required. #### `sound` The alert sound. #### `stopIntent` An `AppIntent` bound to the stop button. #### `secondaryIntent` An `AppIntent` bound to the secondary button. ### Example ```tsx const schedule = AlarmManager.Schedule.fixed( new Date("2026-03-20T07:30:00") ) const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Good morning, time to wake up", stopButton: AlarmManager.Button.create({ title: "Dismiss", textColor: "#ffffff", systemImageName: "xmark" }), secondaryButton: AlarmManager.Button.create({ title: "Later", textColor: "#ffffff", systemImageName: "timer" }), secondaryBehavior: "countdown" }), tintColor: "#34c759", metadata: { category: "morning" } }) const configuration = AlarmManager.Configuration.alarm({ schedule, attributes, sound: AlarmManager.Sound.default() }) ``` *** ## Configuration.timer ```tsx static timer(options: { duration: DurationInSeconds attributes: Attributes sound?: Sound | null stopIntent?: AlarmAppIntent | null secondaryIntent?: AlarmAppIntent | null }): Configuration | null ``` Creates a timer configuration. Suitable for: - Pomodoro timers - Workout timers - Cooking reminders - Short focus sessions ### Parameters #### `duration` The total duration of the timer, in seconds. #### `attributes` The presentation attributes. Required. #### `sound` The alert sound. #### `stopIntent` An `AppIntent` bound to the stop action. #### `secondaryIntent` An `AppIntent` bound to the secondary action. ### Example ```tsx const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "25-minute focus session finished", stopButton: AlarmManager.Button.create({ title: "Done", textColor: "#ffffff", systemImageName: "checkmark" }) }), countdown: AlarmManager.CountdownPresentation.create( "Focusing", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#ff9500", metadata: { mode: "focus" } }) const configuration = AlarmManager.Configuration.timer({ duration: 25 * 60, attributes, sound: AlarmManager.Sound.default() }) ``` *** ## Configuration.countdown ```tsx static countdown(options: { countdown?: Countdown | null schedule?: Schedule | null attributes: Attributes sound?: Sound | null stopIntent?: AlarmAppIntent | null secondaryIntent?: AlarmAppIntent | null }): Configuration | null ``` Creates a countdown reminder configuration. Suitable for: - Triggering a countdown after a scheduled time - “Remind me later” flows - Custom pre-alert and post-alert countdown behavior ### Parameters #### `countdown` The countdown configuration. #### `schedule` The schedule rule. #### `attributes` The presentation attributes. Required. #### `sound` The alert sound. #### `stopIntent` An `AppIntent` bound to the stop action. #### `secondaryIntent` An `AppIntent` bound to the secondary action. ### Example ```tsx const countdown = AlarmManager.Countdown.create({ preAlert: 15 * 60, postAlert: 5 * 60 }) const schedule = AlarmManager.Schedule.relative(21, 0) const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Get ready to rest", stopButton: AlarmManager.Button.create({ title: "OK", textColor: "#ffffff", systemImageName: "bed.double.fill" }), secondaryButton: AlarmManager.Button.create({ title: "Remind me in 15 minutes", textColor: "#ffffff", systemImageName: "timer" }), secondaryBehavior: "countdown" }), countdown: AlarmManager.CountdownPresentation.create( "Rest countdown in progress", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#af52de", metadata: { scene: "night" } }) const configuration = AlarmManager.Configuration.countdown({ countdown, schedule, attributes, sound: AlarmManager.Sound.default() }) ``` *** ## isAvailable ```tsx const isAvailable: boolean ``` Indicates whether AlarmKit is available in the current environment. ```tsx if (!AlarmManager.isAvailable) { throw new Error("AlarmManager is not available") } ``` *** ## alarms ```tsx function alarms(): Promise ``` Returns all current alarms. ### Return Value Returns `Promise`. ### Example ```tsx const list = await AlarmManager.alarms() for (const alarm of list) { console.log(alarm.id, alarm.state) } ``` *** ## schedule ```tsx function schedule(id: string, configuration: Configuration): Promise ``` Creates and schedules an alarm using the provided configuration. ### Parameters #### `id` The unique alarm ID. It must be generated with `UUID.string()`. #### `configuration` A configuration created by `AlarmManager.Configuration.alarm()`, `timer()`, or `countdown()`. ### Return Value Returns the created `Alarm`. ### Example ```tsx const configuration = AlarmManager.Configuration.timer({ duration: 10 * 60, attributes: AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "10 minutes are up", stopButton: AlarmManager.Button.create({ title: "Stop", textColor: "#ffffff", systemImageName: "stop.fill" }) }), countdown: AlarmManager.CountdownPresentation.create( "Counting down", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#007aff" }), sound: AlarmManager.Sound.default() }) const id = UUID.string() const alarm = await AlarmManager.schedule(id, configuration) console.log(alarm.id, alarm.state) ``` *** ## cancel ```tsx function cancel(id: string): Promise ``` Cancels the specified alarm. ### Parameters #### `id` The alarm ID to cancel. ### Return Value Returns whether the operation succeeded. ### Example ```tsx const success = await AlarmManager.cancel(alarmId) console.log("Cancel result:", success) ``` *** ## stop ```tsx function stop(id: string): Promise ``` Stops the specified alarm or alert. This is typically used when an alarm is already alerting, or when the current flow should be terminated. ### Example ```tsx const success = await AlarmManager.stop(alarmId) console.log("Stop result:", success) ``` *** ## pause ```tsx function pause(id: string): Promise ``` Pauses the specified countdown or timer. ### Example ```tsx const success = await AlarmManager.pause(alarmId) console.log("Pause result:", success) ``` *** ## resume ```tsx function resume(id: string): Promise ``` Resumes a paused countdown or timer. ### Example ```tsx const success = await AlarmManager.resume(alarmId) console.log("Resume result:", success) ``` *** ## startCountdown ```tsx function startCountdown(id: string): Promise ``` Starts the countdown flow for the specified alarm. This is usually used with alarms created using `Configuration.countdown()` or flows that include “remind me later” behavior. ### Example ```tsx const success = await AlarmManager.startCountdown(alarmId) console.log("Start countdown result:", success) ``` *** ## addAlarmUpdateListener ```tsx function addAlarmUpdateListener(listener: AlarmUpdateListener): void ``` Adds an alarm update listener. When the alarm list changes, the callback receives the latest `Alarm[]`. ### Example ```tsx const listener = (alarms: AlarmManager.Alarm[]) => { console.log("Alarm count:", alarms.length) for (const alarm of alarms) { console.log(alarm.id, alarm.state) } } AlarmManager.addAlarmUpdateListener(listener) ``` *** ## removeAlarmUpdateListener ```tsx function removeAlarmUpdateListener(listener?: AlarmUpdateListener): void ``` Removes a previously added listener, or removes all listeners if no argument is provided. ### Parameters #### `listener` The listener function to remove. If this argument is omitted, the exact behavior depends on your underlying implementation. ### Example ```tsx const listener = (alarms: AlarmManager.Alarm[]) => { console.log("Updated:", alarms.length) } AlarmManager.addAlarmUpdateListener(listener) AlarmManager.removeAlarmUpdateListener(listener) ``` *** ## Using AppIntent with AlarmManager `AlarmManager` supports `AppIntent` through `stopIntent` and `secondaryIntent`. This allows alert buttons to do more than just stop an alert or start a countdown. They can also run custom script logic, for example: - Marking a task as completed when the user taps Stop - Updating local state when an alert is dismissed - Refreshing Widgets or Live Activities after “Later” is tapped - Triggering additional business logic through global APIs *** ## Where AppIntent Must Be Defined All `AppIntent` definitions must be placed in: ```tsx app_intents.tsx ``` Do not define them in `widget.tsx`, `live_activity.tsx`, `control_widget.tsx`, or regular script files. When an `AppIntent` runs: ```tsx Script.env === "app_intents" ``` That means the `perform` function always executes in the `app_intents` environment. *** ## AppIntent Types ### AppIntent Represents a concrete app intent instance. | Field | Type | Description | | ---------- | ------------------- | -------------------------------------------------- | | `script` | `string` | The script path, generated internally | | `name` | `string` | The unique intent name | | `protocol` | `AppIntentProtocol` | The protocol type used by the intent | | `params` | `T` | The parameters passed when the intent is triggered | ### AppIntentFactory ```tsx type AppIntentFactory = (params: T) => AppIntent ``` A factory function that creates an `AppIntent` instance from parameters. ### AppIntentPerform ```tsx type AppIntentPerform = (params: T) => Promise ``` The async function that runs when the intent is triggered. ### AppIntentProtocol | Enum Member | Description | | ---------------------- | ------------------------------------------------------------ | | `AppIntent` | A regular app intent | | `AudioPlaybackIntent` | An intent for controlling audio playback | | `AudioRecordingIntent` | An intent for controlling audio recording | | `LiveActivityIntent` | An intent for starting, pausing, or updating Live Activities | For `AlarmManager`, you should use: ```tsx AppIntentProtocol.LiveActivityIntent ``` *** ## AppIntentManager.register ```tsx static register(options: { name: string protocol: AppIntentProtocol perform: AppIntentPerform }): AppIntentFactory ``` Registers a new `AppIntent`. ### Parameters #### `name` The unique intent name. #### `protocol` The protocol type for the intent. #### `perform` The async function that runs when the intent is triggered. ### Return Value Returns a factory function that creates the corresponding `AppIntent` instance. *** ## AppIntent Example for AlarmManager The example below shows how to define two intents in `app_intents.tsx` for use with `AlarmManager`. ```tsx /// app_intents.tsx export const StopFocusAlarmIntent = AppIntentManager.register({ name: "StopFocusAlarmIntent", protocol: AppIntentProtocol.LiveActivityIntent, perform: async ({ alarmId, taskId }: { alarmId: string; taskId: string }) => { console.log("Stopping alert:", alarmId, taskId) // Your custom logic goes here // For example, update task state, save logs, refresh UI, etc. await Storage.set(`task:${taskId}:finished`, true) Widget.reloadAll() } }) export const SnoozeFocusAlarmIntent = AppIntentManager.register({ name: "SnoozeFocusAlarmIntent", protocol: AppIntentProtocol.LiveActivityIntent, perform: async ({ alarmId }: { alarmId: string }) => { console.log("Snoozing alert:", alarmId) Widget.reloadAll() } }) ``` *** ## Binding AppIntent to AlarmManager Once the intents are defined, they can be attached through `stopIntent` and `secondaryIntent`. ```tsx const alarmId = UUID.string() const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Focus session finished", stopButton: AlarmManager.Button.create({ title: "Done", textColor: "#ffffff", systemImageName: "checkmark" }), secondaryButton: AlarmManager.Button.create({ title: "Snooze 5 min", textColor: "#ffffff", systemImageName: "timer" }), secondaryBehavior: "custom" }), countdown: AlarmManager.CountdownPresentation.create( "Counting down", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#007aff", metadata: { type: "focus" } }) const configuration = AlarmManager.Configuration.timer({ duration: 25 * 60, attributes, sound: AlarmManager.Sound.default(), stopIntent: StopFocusAlarmIntent({ alarmId, taskId: "task_001" }), secondaryIntent: SnoozeFocusAlarmIntent({ alarmId }) }) await AlarmManager.schedule(alarmId, configuration) ``` In this example: - The stop button triggers `StopFocusAlarmIntent` - The secondary button triggers `SnoozeFocusAlarmIntent` - The secondary button uses `"custom"`, so its behavior is fully defined by `secondaryIntent` *** ## Using Countdown Behavior for the Secondary Button If you want the secondary button to directly use AlarmKit’s countdown behavior, set: ```tsx secondaryBehavior: "countdown" ``` Example: ```tsx const alarmId = UUID.string() const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Time to rest", stopButton: AlarmManager.Button.create({ title: "Dismiss", textColor: "#ffffff", systemImageName: "xmark" }), secondaryButton: AlarmManager.Button.create({ title: "Remind me in 10 minutes", textColor: "#ffffff", systemImageName: "timer" }), secondaryBehavior: "countdown" }), countdown: AlarmManager.CountdownPresentation.create( "Snooze countdown in progress", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#ff3b30" }) const configuration = AlarmManager.Configuration.countdown({ countdown: AlarmManager.Countdown.create({ preAlert: 10 * 60 }), attributes, sound: AlarmManager.Sound.default() }) await AlarmManager.schedule(alarmId, configuration) ``` This approach is better for standard snooze-style interactions. *** ## Creating a One-Time Alarm ```tsx if (!AlarmManager.isAvailable) { throw new Error("AlarmKit is not available in the current environment") } const alarmId = UUID.string() const schedule = AlarmManager.Schedule.fixed( new Date("2026-03-20T22:30:00") ) const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Time to rest", stopButton: AlarmManager.Button.create({ title: "Dismiss", textColor: "#ffffff", systemImageName: "xmark" }), secondaryButton: AlarmManager.Button.create({ title: "Remind me in 10 minutes", textColor: "#ffffff", systemImageName: "timer" }), secondaryBehavior: "countdown" }), countdown: AlarmManager.CountdownPresentation.create( "Snooze countdown in progress", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#ff3b30", metadata: { purpose: "sleep" } }) const configuration = AlarmManager.Configuration.alarm({ schedule, attributes, sound: AlarmManager.Sound.default() }) const alarm = await AlarmManager.schedule(alarmId, configuration) console.log("Created:", alarm.id, alarm.state) ``` *** ## Creating a Weekly Recurring Alarm ```tsx const alarmId = UUID.string() const schedule = AlarmManager.Schedule.weekly(7, 30, [2, 3, 4, 5, 6]) const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Weekday wake-up reminder", stopButton: AlarmManager.Button.create({ title: "Wake Up", textColor: "#ffffff", systemImageName: "sun.max.fill" }) }), tintColor: "#34c759", metadata: { tag: "weekday-morning" } }) const configuration = AlarmManager.Configuration.alarm({ schedule, attributes, sound: AlarmManager.Sound.default() }) await AlarmManager.schedule(alarmId, configuration) ``` *** ## Creating a Pomodoro Timer ```tsx const alarmId = UUID.string() const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Your focus session has ended", stopButton: AlarmManager.Button.create({ title: "Done", textColor: "#ffffff", systemImageName: "checkmark.circle.fill" }) }), countdown: AlarmManager.CountdownPresentation.create( "Focusing", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Focus paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#ff9500", metadata: { type: "pomodoro", duration: "1500" } }) const configuration = AlarmManager.Configuration.timer({ duration: 25 * 60, attributes, sound: AlarmManager.Sound.default() }) await AlarmManager.schedule(alarmId, configuration) ``` *** ## Creating a Manually Started Countdown Reminder ```tsx const alarmId = UUID.string() const countdown = AlarmManager.Countdown.create({ preAlert: 5 * 60, postAlert: 60 }) const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Snooze time is over", stopButton: AlarmManager.Button.create({ title: "Got it", textColor: "#ffffff", systemImageName: "bell.slash.fill" }) }), countdown: AlarmManager.CountdownPresentation.create( "Snooze in progress", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Countdown paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#af52de" }) const configuration = AlarmManager.Configuration.countdown({ countdown, attributes, sound: AlarmManager.Sound.default() }) await AlarmManager.schedule(alarmId, configuration) await AlarmManager.startCountdown(alarmId) ``` *** ## Querying and Controlling Alarms ```tsx const alarms = await AlarmManager.alarms() console.log("Alarm count:", alarms.length) const firstAlarm = alarms[0] if (firstAlarm) { await AlarmManager.pause(firstAlarm.id) await AlarmManager.resume(firstAlarm.id) await AlarmManager.stop(firstAlarm.id) } ``` *** ## Listening for Alarm State Changes ```tsx const listener = (alarms: AlarmManager.Alarm[]) => { const summary = alarms.map(item => ({ id: item.id, state: item.state })) console.log("Latest alarm states:", JSON.stringify(summary, null, 2)) } AlarmManager.addAlarmUpdateListener(listener) // Remove when no longer needed // AlarmManager.removeAlarmUpdateListener(listener) ``` *** ## Execution Environment `AlarmManager` itself is a global API and can be used directly in regular scripts. However, if you attach `AppIntent` objects to `AlarmManager`, those intent `perform` functions always run in: ```tsx Script.env === "app_intents" ``` That means: - Alarm creation logic such as `AlarmManager.schedule()` usually runs in a normal script environment - Logic for `stopIntent` and `secondaryIntent` runs in the `perform` functions defined in `app_intents.tsx` - Inside `perform`, you can safely do things like network requests, state updates, Widget refreshes, and Live Activity updates *** ## Best Practices ### Always use UUID.string() for alarmId This is a strict requirement of `AlarmManager`. ```tsx const alarmId = UUID.string() ``` ### Keep the generated alarmId Once an alarm is created, keep the `alarmId` if you need to pause, resume, stop, or cancel it later. ### Use metadata for business-related information Store task IDs, source pages, scene names, or other app-specific values there. ```tsx metadata: { taskId: "task_123", scene: "study", source: "focus-page" } ``` ### Keep all AppIntent definitions in app\_intents.tsx Do not scatter them across multiple files. ### Use explicit parameter types for AppIntent This improves type checking and editor completion. ```tsx perform: async ({ alarmId, taskId }: { alarmId: string; taskId: string }) => { // ... } ``` ### Prefer countdown for standard snooze behavior If you only need a normal “remind me again in a few minutes” flow, prefer: ```tsx secondaryBehavior: "countdown" ``` If you need completely custom logic, use: ```tsx secondaryBehavior: "custom" secondaryIntent: ... ``` *** ## Notes ### Attributes.create(...) may return null ```tsx const attributes = AlarmManager.Attributes.create(...) ``` Its return type is `Attributes | null`, so you should validate the result in stricter flows. ### Configuration methods may also return null ```tsx const configuration = AlarmManager.Configuration.timer(...) ``` These methods also allow `null`, so validate before calling `schedule()`. ### pause, resume, and startCountdown are not meaningful for every alarm type These methods are generally intended for timers or countdown-based flows. For fixed-time alarms, some operations may not make sense. *** ## Full Example The example below shows a complete flow that includes `AppIntent`. ```tsx /// app_intents.tsx export const StopPomodoroIntent = AppIntentManager.register({ name: "StopPomodoroIntent", protocol: AppIntentProtocol.LiveActivityIntent, perform: async ({ alarmId, sessionId }: { alarmId: string; sessionId: string }) => { console.log("Pomodoro finished:", alarmId, sessionId) await Storage.set(`pomodoro:${sessionId}:done`, true) Widget.reloadAll() } }) export const ExtendPomodoroIntent = AppIntentManager.register({ name: "ExtendPomodoroIntent", protocol: AppIntentProtocol.LiveActivityIntent, perform: async ({ alarmId, extraMinutes }: { alarmId: string; extraMinutes: number }) => { console.log("Extending Pomodoro:", alarmId, extraMinutes) Widget.reloadAll() } }) ``` ```tsx /// regular script file async function main() { if (!AlarmManager.isAvailable) { console.log("AlarmManager is not available") return } const listener = (alarms: AlarmManager.Alarm[]) => { console.log( "Alarm update:", alarms.map(item => `${item.id}:${item.state}`).join(", ") ) } AlarmManager.addAlarmUpdateListener(listener) const alarmId = UUID.string() const attributes = AlarmManager.Attributes.create({ alert: AlarmManager.AlertPresentation.create({ title: "Focus session finished", stopButton: AlarmManager.Button.create({ title: "Done", textColor: "#ffffff", systemImageName: "checkmark" }), secondaryButton: AlarmManager.Button.create({ title: "Extend 5 min", textColor: "#ffffff", systemImageName: "timer" }), secondaryBehavior: "custom" }), countdown: AlarmManager.CountdownPresentation.create( "Focusing", AlarmManager.Button.create({ title: "Pause", textColor: "#ffffff", systemImageName: "pause.fill" }) ), paused: AlarmManager.PausedPresentation.create( "Paused", AlarmManager.Button.create({ title: "Resume", textColor: "#ffffff", systemImageName: "play.fill" }) ), tintColor: "#007aff", metadata: { feature: "focus-mode" } }) if (!attributes) { throw new Error("Failed to create Attributes") } const configuration = AlarmManager.Configuration.timer({ duration: 15 * 60, attributes, sound: AlarmManager.Sound.default(), stopIntent: StopPomodoroIntent({ alarmId, sessionId: "session_001" }), secondaryIntent: ExtendPomodoroIntent({ alarmId, extraMinutes: 5 }) }) if (!configuration) { throw new Error("Failed to create Configuration") } const alarm = await AlarmManager.schedule(alarmId, configuration) console.log("Created alarm:", alarm.id, alarm.state) const current = await AlarmManager.alarms() console.log("Current alarms:", current.map(item => item.id)) await AlarmManager.pause(alarmId) await AlarmManager.resume(alarmId) // Remove when finished // AlarmManager.removeAlarmUpdateListener(listener) } main().catch(error => { console.log(String(error)) }) ``` --- url: /guide/Changelog/3.0.0/FileManager.md --- # FileManager The **FileManager** module provides a unified interface for interacting with the file system in Scripting. It serves as the primary API for accessing local files, iCloud files, App Group shared storage, and performing common file operations such as reading, writing, copying, moving, deleting, zipping, and unzipping. *** ## Core Properties ### `FileManager.scriptsDirectory: string` Returns the directory path where script files are stored within the Scripting app. ### `FileManager.isiCloudEnabled: boolean` Indicates whether iCloud is enabled for the user. Returns `false` if: - The user is not logged into iCloud, or - iCloud permissions have not been granted to the Scripting app. ### `FileManager.iCloudDocumentsDirectory: string` Returns the path to the iCloud `Documents` directory. Throws an error if iCloud is disabled. Always check `isiCloudEnabled` before calling. ### `FileManager.appGroupDocumentsDirectory: string` Returns the path to the shared App Group `Documents` directory. Files stored here are not accessible in the Files app, but scripts running inside Widgets can access them. ### `FileManager.isWebDAVAvailable: boolean` Indicates whether WebDAV has been configured and is available for use. ### `FileManager.webDAVDocumentsDirectory: string` Returns the path to the WebDAV-backed `Documents` cache directory. Files written here will be queued for WebDAV sync after WebDAV is configured. ### `FileManager.safariBrowserDirectory: string` Returns the Safari browser userscript data root. This directory follows the Safari Browser Data storage location configured in Settings and contains `userscripts/`, `storages/`, and `downloads/`. Use this when an app script needs to inspect or maintain the whole Safari extension data area. ### `FileManager.safariBrowserStorageDirectory: string` Returns the directory where Safari browser userscripts store GM value JSON files. This points to `scripting-safari-extension/storages/`. ### `FileManager.safariBrowserDownloadsDirectory: string` Returns the directory where Safari browser userscripts save files through `GM.download`. This points to `scripting-safari-extension/downloads/`. ### `FileManager.safariBrowserUserscriptsDirectory: string` Returns the directory where Safari browser userscripts installed from the extension popup are stored. This points to `scripting-safari-extension/userscripts/`. ### `FileManager.documentsDirectory: string` Returns the path to the local `Documents` directory. Files stored here are visible in the Files app. Widgets cannot access files in this directory. ### `FileManager.temporaryDirectory: string` Returns the path to the system temporary directory. Files stored here may be removed automatically by the system. *** ## iCloud File Management ### `FileManager.isFileStoredIniCloud(filePath: string): boolean` Checks whether the specified file is stored in iCloud. | Parameter | Type | Description | | --------- | ------ | ---------------------- | | filePath | string | The file path to check | ### `FileManager.isiCloudFileDownloaded(filePath: string): boolean` Checks whether an iCloud-stored file has been downloaded to the device. ### `FileManager.downloadFileFromiCloud(filePath: string): Promise` Downloads an iCloud file to the device. | Returns | Description | | ----------------- | --------------------------- | | Promise\ | `true` if download succeeds | **Example:** ```ts if (FileManager.isiCloudEnabled) { const file = FileManager.iCloudDocumentsDirectory + "/data.json"; const success = await FileManager.downloadFileFromiCloud(file); } ``` ### `FileManager.getShareUrlOfiCloudFile(path: string, expiration?: number): string` Generates a shareable download URL for an iCloud file. | Parameter | Type | Description | | ---------- | ----------------- | -------------------------------------------------------------------------------------------------------------------- | | path | string | The iCloud file path. Must begin with `FileManager.iCloudDocumentsDirectory`. File must be fully uploaded to iCloud. | | expiration | number (optional) | Expiration timestamp of the share link | This call may throw an error. Use `try-catch` for error handling. *** ## Directory and File Operations All operations are available in both asynchronous and synchronous forms. Synchronous methods block execution; asynchronous versions are recommended for most use cases. ### Create Directory #### `createDirectory(path: string, recursive?: boolean): Promise` #### `createDirectorySync(path: string, recursive?: boolean): void` | Parameter | Type | Description | | --------- | ------------------ | ------------------------------------------------------- | | path | string | Directory path to create | | recursive | boolean (optional) | If true, creates intermediate directories automatically | ### Create Symbolic Link #### `createLink(path: string, target: string): Promise` #### `createLinkSync(path: string, target: string): void` Creates a symbolic link at `path` pointing to `target`. ### Copy File #### `copyFile(path: string, newPath: string): Promise` #### `copyFileSync(path: string, newPath: string): void` ### Read Directory #### `readDirectory(path: string, recursive?: boolean): Promise` #### `readDirectorySync(path: string, recursive?: boolean): string[]` Enumerates the contents of a directory and optionally recurses into subdirectories. ### Check Existence #### `exists(path: string): Promise` #### `existsSync(path: string): boolean` Checks whether a file or directory exists. ### File Bookmarks Bookmarks allow persistent access to user-authorized external files or folders. | Method | Description | | ----------------------- | --------------------------------------------------------------- | | `bookmarkExists(name)` | Checks whether a bookmark exists | | `getAllFileBookmarks()` | Returns all bookmark names and their paths | | `bookmarkedPath(name)` | Returns file path for the bookmark name, or `null` if not found | *** ### Determine File Type | Method | Returns | Description | | --------------------------------- | ------- | ------------------------------------ | | `isFile / isFileSync` | boolean | Whether the path refers to a file | | `isDirectory / isDirectorySync` | boolean | Whether it refers to a directory | | `isLink / isLinkSync` | boolean | Whether it refers to a symbolic link | | `isBinaryFile / isBinaryFileSync` | boolean | Whether the file is binary | *** ## File Reading and Writing Supports three data formats: string, `Uint8Array`, and `Data`. ### Read File | Method | Return Type | Description | | ------------------- | ----------- | ---------------------------------------- | | readAsString / Sync | string | Reads text using specified encoding | | readAsBytes / Sync | Uint8Array | Reads raw bytes | | readAsData / Sync | Data | Reads the entire file as a `Data` object | ### Write File | Method | Data Format | | -------------------- | ----------- | | writeAsString / Sync | string | | writeAsBytes / Sync | Uint8Array | | writeAsData / Sync | Data | Existing files will be overwritten. ### Append to File | Method | Data Format | | ----------------- | ----------- | | appendText / Sync | string | | appendData / Sync | Data | If the file or its parent directory does not exist, it will be created automatically. *** ## File Information and Management ### `stat(path: string): Promise` ### `statSync(path: string): FileStat` Returns a `FileStat` object for the file or directory. If the path is a symbolic link, the resolved target is reported. ### `rename / renameSync` Moves or renames a file or directory. ### `remove / removeSync` Removes a file or directory. Directories are removed recursively. *** ## Compression and Extraction ### `zip(srcPath: string, destPath: string, shouldKeepParent?: boolean): Promise` ### `zipSync(srcPath: string, destPath: string, shouldKeepParent?: boolean): void` Creates a zip archive from a file or directory. ### `unzip(srcPath: string, destPath: string): Promise` ### `unzipSync(srcPath: string, destPath: string): void` Extracts a zip archive to the specified destination. **Example:** ```ts const docs = FileManager.documentsDirectory; await FileManager.zip(docs + "/MyScript", docs + "/MyScript.zip"); await FileManager.unzip(docs + "/MyScript.zip", docs + "/Output"); ``` *** ## Utility Methods ### `mimeType(path: string): string` Returns the MIME type of the file based on its extension. ### `destinationOfSymbolicLink(path: string): string` Returns the destination of a symbolic link. *** ## Types ### `FileStat` ```ts type FileStat = { creationDate: number; modificationDate: number; type: string; // "file" | "directory" | "link" | "unixDomainSock" | "pipe" | "notFound" size: number; }; ``` --- url: /guide/Changelog/3.0.0/TranslationUIProvider.md --- # TranslationUIProvider `TranslationUIProvider` allows scripts to take control of the system translation panel’s UI and interaction flow. It enables developers to build fully customized translation interfaces and implement their own default translation logic. When a host app invokes the translation extension, Scripting executes the `translation_ui_provider.tsx` script. Within this environment, developers can access the source text provided by the host, perform translation logic, construct a custom UI, and present it using `TranslationUIProvider.present(...)`. This capability is suitable for scenarios such as: - Customizing the layout and interaction of the translation panel - Displaying source text, translated results, language selectors, and controls - Integrating custom translation services or pipelines - Returning translated text to replace the original content (if allowed by the host) - Providing a read-only translation interface without modifying the original text `TranslationUIProvider` does not perform translation itself. It provides a UI container and session control interface. Translation logic must be implemented by the developer within the script, such as calling remote APIs or local processing modules. *** ## Availability `TranslationUIProvider` requires **iOS 18.4 or later**. *** ## Availability `TranslationUIProvider` is only available within the Translation UI Provider execution environment and should be used in the `translation_ui_provider.tsx` file. When the system presents the translation panel, this script acts as the entry point of the session. A typical workflow includes: 1. Read the source text from the host 2. Execute default translation logic 3. Build and present a custom UI 4. Handle user interactions (expand panel, confirm result, finish session) *** ## Execution Model `TranslationUIProvider` serves two main purposes: - Providing access to the current translation session context (e.g., source text, replacement capability) - Controlling the lifecycle and UI of the translation panel The session is created by the system and host app. Scripting automatically injects the current session into the `TranslationUIProvider` namespace when executing `translation_ui_provider.tsx`. *** ## Session Input ### inputText ```ts const inputText: string | null ``` Represents the source text provided by the host app. This is typically the text selected by the user or passed by the host for translation. The value may be `null`, so scripts must handle this case explicitly. Common usage: - Display the source text in UI - Use it as input for translation logic - Show fallback UI when no text is available Example: ```ts const source = TranslationUIProvider.inputText if (!source) { TranslationUIProvider.present( No text available for translation. ) } ``` `inputText` reflects the initial session input and should not be assumed to always exist. *** ### allowsReplacement ```ts const allowsReplacement: boolean ``` Indicates whether the host app allows replacing the original text with the translated result. If `true`, scripts can return a translation via `finish(translatedText)`. If `false`, translations can only be displayed, not applied. Typical usage: - Conditionally show “Replace” or “Use Translation” actions - Adjust UI behavior based on capability Example: ```ts const canReplace = TranslationUIProvider.allowsReplacement ``` When replacement is not allowed, the UI should avoid presenting misleading actions. *** ## Panel Control ### present(node) ```ts function present(node: VirtualNode): void ``` Displays the scripted UI in the translation panel. This is the primary entry point for rendering UI. Developers should construct a `VirtualNode` and pass it to `present(...)`. Example: ```tsx import { Text, VStack } from "scripting" TranslationUIProvider.present( Custom Translation Panel {TranslationUIProvider.inputText ?? "No input text"} ) ``` It is recommended to encapsulate the UI into a component: ```tsx function TranslationView() { const source = TranslationUIProvider.inputText return Original {source ?? "No input text"} } TranslationUIProvider.present() ``` `present(...)` only renders the UI. It does not end the session or return results. *** ### expandSheet() ```ts function expandSheet(): void ``` Requests the system to expand the translation panel. This is useful when: - Displaying long content - Showing multiple translation results - Providing advanced controls Example: ```ts TranslationUIProvider.expandSheet() ``` This is a request, not a guarantee. The system decides how to handle it. *** ### finish(translation?) ```ts function finish(translation?: string | null): void ``` Ends the current translation session and optionally returns a translated result. Two common patterns: Close without replacement: ```ts TranslationUIProvider.finish() ``` Close and return translation: ```ts TranslationUIProvider.finish("Hello world") ``` Typical usage: ```ts if (TranslationUIProvider.allowsReplacement) { TranslationUIProvider.finish(translatedText) } else { TranslationUIProvider.finish() } ``` Notes: - Calling `finish(...)` ends the session immediately - Passing a string attempts to return it to the host - Passing `null` or omitting closes without replacement - Final behavior depends on host capabilities *** ## Recommended Development Pattern A typical structure in `translation_ui_provider.tsx`: 1. Read `inputText` 2. Handle empty input 3. Execute translation logic 4. Build UI with source and translated text 5. Let user confirm or cancel 6. Call `finish(...)` accordingly *** ## Example: Minimal Translation Panel ```tsx import { Button, Text, VStack } from "scripting" function TranslationView() { const source = TranslationUIProvider.inputText return Original Text {source ?? "No text available"} ))} ) } async function run() { await Navigation.present({ element: }) Script.exit() } run() ``` --- url: /guide/Changelog/3.1.0/AVCaptureSession & Camera Control/Live Photo/index.md --- # Live Photo A Live Photo is a photo _plus_ a short `.mov` companion clip captured around the shutter. `AVCapturePhotoOutput` lets you opt in per capture — pass `livePhotoMovieFile` to `capturePhoto` and `capturePhoto` resolves with both the still image and the path of the saved movie. ## Prerequisites - The photo output must have `isLivePhotoCaptureSupported === true` (true on all camera-capable iPhones). - You must enable it once on the output: `photoOutput.isLivePhotoCaptureEnabled = true`. The bridge silently clamps this to `false` on outputs that don't support Live Photo, so a follow-up read is the simplest feature gate. - If `isLivePhotoCaptureEnabled` is `false` at the moment `capturePhoto` is called with `livePhotoMovieFile`, the promise **rejects immediately** rather than performing a non-Live capture. - Do not enable `isAutoDeferredPhotoDeliveryEnabled` at the same time. With deferred on, the system may finalize without producing the `.mov`, and `livePhotoMovieFileURL` will be absent from the resolved value. ## Wiring it up ```ts const camera = AVCaptureDevice.default("video")! const session = new AVCaptureSession() const photoOutput = new AVCapturePhotoOutput() session.configure(() => { session.sessionPreset = "photo" session.addInput(new AVCaptureDeviceInput(camera)) session.addOutput(photoOutput) }) // One-time setup. The flag survives across capturePhoto calls. photoOutput.isLivePhotoCaptureEnabled = true await session.startRunning() const ts = Date.now() const photoFile = `${FileManager.documentsDirectory}/live_${ts}.heic` const movieFile = `${FileManager.documentsDirectory}/live_${ts}.mov` const result = await photoOutput.capturePhoto({ codec: "hevc", photoFile, // ← required if you'll later call Photos.saveLivePhoto livePhotoMovieFile: movieFile, livePhotoVideoCodec: "hevc", }) console.log("photo:", result.image.width, "×", result.image.height) console.log("still file:", result.photoFileURL) console.log("movie:", result.livePhotoMovieFileURL) ``` The resolved object has the usual `image / metadata / isRawPhoto / isDeferredProxy` plus an extra `photoFileURL: string` (if you passed `photoFile`) and `livePhotoMovieFileURL: string` (if you passed `livePhotoMovieFile`). If you didn't request a Live Photo, the `.mov` field is absent. ### Why `photoFile` matters for Live Photo `capturePhoto` always gives you `result.image: UIImage` — but `UIImage` is a decoded bitmap with no original metadata. When you save a Live Photo to the system Photo Library via `Photos.saveLivePhoto(...)`, PhotoKit verifies that the still and the `.mov` share a Live Photo asset identifier (embedded in the Apple Maker Note under key 17 on the still; in `com.apple.quicktime.content.identifier` on the movie). Re-encoding the `UIImage` via `image.toJPEGData()` strips that identifier and PhotoKit rejects the pair with **`PHPhotosErrorDomain 3302`**. The `photoFile` option avoids this by asking the bridge to write `photo.fileDataRepresentation()` straight to disk — that's the raw bytes the camera produced, with the Maker Note intact. Feed `result.photoFileURL` into `Photos.saveLivePhoto({ imagePath, videoPath })` and pairing works. ## What "resolve" actually waits for A Live Photo capture fires two parallel AVFoundation callbacks: one for the still image and one for the `.mov` file. The bridge waits for **both** before resolving — you always get a fully usable pair, or an error. AVFoundation does not guarantee an order between the two, so don't rely on file timestamps to infer it. The system also runs a "capture finish" sweep at the end. If something goes wrong half-way (e.g. the device drops the `.mov`), the bridge falls back to whatever has been delivered so far rather than hanging the promise. ## File path rules - Pass an **absolute path** ending in `.mov`. `${FileManager.documentsDirectory}/...` is the easiest source of a writable path. - AVFoundation refuses to write to an existing path. The bridge deletes the file at the requested path **before** the capture starts to spare you that error. - The `.mov` is small (\~2–4 MB for a 1.5 s clip). Clean up old files yourself if you're capturing repeatedly. ## Codec selection `livePhotoVideoCodec` is optional. Acceptable values: - `"hevc"` — preferred on iPhone 7 and later (supported devices). Smaller files. - `"h264"` — wider compatibility (older systems, some editing tools). If you pass a codec that the device doesn't list in `availableLivePhotoVideoCodecTypes`, the bridge silently lets AVFoundation pick its default rather than failing the capture. Read the resolved file with an `AVAsset` if you need to know what was actually written. ## Things to know - Live Photo + `flashMode = "on"` works, but the captured movie includes the flash flicker; design for that. - The `.mov` you get is exactly what Photos.app would save — it includes a slight pre-roll and post-roll around the shutter. - `capturePhoto` only writes files to disk — it doesn't put anything in the system Photo Library. To save the photo and clip as a single linked Live Photo (the way Camera.app does it), pass both paths to [`Photos.saveLivePhoto`](#): ```ts await Photos.saveLivePhoto({ imagePath: result.photoFileURL!, // raw HEIC from `photoFile` option videoPath: result.livePhotoMovieFileURL!, // .mov from `livePhotoMovieFile` option shouldMoveFile: true, // move both into Photos rather than copy }) ``` This pairs them into one PHAsset; Photos.app shows the long-press "Live" animation on it. The first call triggers the system Photo Library permission prompt. **Do not** substitute `result.image.toJPEGData()` for `result.photoFileURL` — the re-encoded JPEG loses the Live Photo asset identifier and PhotoKit rejects the pairing. --- url: /guide/Changelog/3.1.0/AVCaptureSession & Camera Control/Live Photo/index_example.md --- # Example ```tsx import { Button, CaptureVideoPreviewView, HStack, Image, Navigation, NavigationStack, Script, Spacer, Text, Toolbar, ToolbarItem, useEffect, useMemo, useObservable, VStack } from "scripting" type Shot = { index: number image: UIImage photoSize: string /** native 写盘的原始 still bytes(HEIC/JPEG),含 Live Photo asset identifier */ photoURL?: string movieURL?: string movieSize?: number ms: number isDeferredProxy: boolean } function View() { const dismiss = Navigation.useDismiss() const isRunning = useObservable(false) const shots = useObservable([]) const captureCount = useObservable(0) const lastError = useObservable("") const livePhotoSupp = useObservable(false) const livePhotoEn = useObservable(false) const { session, camera, photoOutput } = useMemo(() => { const camera = AVCaptureDevice.default("video")! const session = new AVCaptureSession() const input = new AVCaptureDeviceInput(camera) const photoOutput = new AVCapturePhotoOutput() session.configure(() => { session.sessionPreset = "photo" if (session.canAddInput(input)) session.addInput(input) if (session.canAddOutput(photoOutput)) session.addOutput(photoOutput) }) // 必须在 addOutput 之后查 supported,在那之前 photoOutput 没绑 session 时 supported=false。 return { session, camera, photoOutput } }, []) useEffect(() => { async function start() { try { await session.startRunning() isRunning.setValue(true) // Live Photo 必须开启 enabled 之后才会真正生效。 photoOutput.isLivePhotoCaptureEnabled = true // 读回真实值: 不支持的设备 setter 会被 clamp 回 false。 livePhotoSupp.setValue(photoOutput.isLivePhotoCaptureSupported) livePhotoEn.setValue(photoOutput.isLivePhotoCaptureEnabled) console.log("[startRunning]", { preset: session.sessionPreset, liveSupp: photoOutput.isLivePhotoCaptureSupported, liveEn: photoOutput.isLivePhotoCaptureEnabled, }) } catch (e) { await Dialog.alert({ message: `Failed to start: ${String(e)}` }) dismiss() } } start() return () => { session.stopRunning().finally(() => session.dispose()) } }, []) async function takeLivePhoto() { const idx = captureCount.value + 1 captureCount.setValue(idx) const ts = Date.now() // 同时让 native 写盘 still + .mov。两份文件共享 Apple Maker Note 里的 // Live Photo asset identifier, Photos.saveLivePhoto 才能 pair 上。 const photoFile = `${FileManager.documentsDirectory}/live_${idx}_${ts}.heic` const movieFile = `${FileManager.documentsDirectory}/live_${idx}_${ts}.mov` const start = Date.now() try { const result = await photoOutput.capturePhoto({ codec: "hevc", photoFile, livePhotoMovieFile: movieFile, livePhotoVideoCodec: "hevc", }) const ms = Date.now() - start // 读 movie 文件大小供 UI 显示 let movieSize: number | undefined if (result.livePhotoMovieFileURL) { try { const stat = await FileManager.stat(result.livePhotoMovieFileURL) movieSize = stat.size } catch { /* ignore */ } } shots.setValue([{ index: idx, image: result.image, photoSize: `${result.image.width}×${result.image.height}`, photoURL: result.photoFileURL, movieURL: result.livePhotoMovieFileURL, movieSize, ms, isDeferredProxy: result.isDeferredProxy, }, ...shots.value].slice(0, 6)) lastError.setValue("") } catch (e) { lastError.setValue(String(e)) } } /** 不开 Live Photo,验证退化路径:resolve 不应包含 livePhotoMovieFileURL */ async function takeRegular() { const idx = captureCount.value + 1 captureCount.setValue(idx) const start = Date.now() try { const result = await photoOutput.capturePhoto({ codec: "hevc" }) shots.setValue([{ index: idx, image: result.image, photoSize: `${result.image.width}×${result.image.height}`, movieURL: result.livePhotoMovieFileURL, // 应为 undefined ms: Date.now() - start, isDeferredProxy: result.isDeferredProxy, }, ...shots.value].slice(0, 6)) lastError.setValue("") } catch (e) { lastError.setValue(String(e)) } } /** * 把最新一张 Live Photo 配对存进系统照片库。 * 关键: `imagePath` 必须用 native 写盘的 `result.photoFileURL`(原始 HEIC, * 含 Apple Maker Note 里的 Live Photo asset identifier),用 toJPEGData 重编码 * 出来的 JPEG 会丢这个 identifier, Photos.saveLivePhoto 报 PHPhotosError 3302。 * `shouldMoveFile: true` 会把磁盘上两个文件 move 进 Photos,本地副本就消失了。 */ async function saveLast() { const last = shots.value[0] if (!last) { lastError.setValue("Capture a Live Photo first") return } if (!last.photoURL || !last.movieURL) { lastError.setValue("Last capture missing photoURL or movieURL (was it a Regular shot?)") return } try { await Photos.saveLivePhoto({ imagePath: last.photoURL, videoPath: last.movieURL, shouldMoveFile: true, }) lastError.setValue(`Saved Live Photo #${last.index} to Photos library`) } catch (e) { lastError.setValue(`Save failed: ${String(e)}`) console.error(e) } } /** 故意关掉 enabled 再调 Live Photo,期望 promise reject */ async function probeReject() { photoOutput.isLivePhotoCaptureEnabled = false livePhotoEn.setValue(photoOutput.isLivePhotoCaptureEnabled) try { await photoOutput.capturePhoto({ livePhotoMovieFile: `${FileManager.documentsDirectory}/should_reject.mov`, }) lastError.setValue("Unexpected: did not reject") } catch (e) { lastError.setValue(`OK (rejected): ${String(e)}`) } finally { // 恢复 enabled 方便继续测 photoOutput.isLivePhotoCaptureEnabled = true livePhotoEn.setValue(photoOutput.isLivePhotoCaptureEnabled) } } return ( })} {resp != null ? <> : <> } {/* 未选中的先画(细),选中的最后画压在上面(粗)。MapPolyline 目前不接收 opacity / 视图层 modifier,这里用 stroke width(3 vs 6)制造主次对比。 */} {routes.map((r, i) => { if (i === selectedIdx) return null return })} {routes[selectedIdx] != null ? : null} } // ─────────────────────────────────────────────────────────────────────── // Demo 4: calculateETA — time / distance only, no geometry download // ─────────────────────────────────────────────────────────────────────── function ETADemo() { type Mode = "automobile" | "walking" const [mode, setMode] = useState("automobile") const [eta, setEta] = useState(null) const [loading, setLoading] = useState(false) const [err, setErr] = useState(null) const calc = async () => { setLoading(true); setErr(null) try { const result = await MapDirections.calculateETA({ source: { coordinate: PEOPLES_SQUARE, name: "People's Square" }, destination: { coordinate: LUJIAZUI, name: "Lujiazui" }, transportType: mode, departureDate: new Date(), }) setEta(result) } catch (e) { setErr(String(e)) } finally { setLoading(false) } } return 4. `calculateETA` Skip the polyline download; get back travel time / distance / arrival window only. Cheaper than `calculate` when the UI just needs a headline number. setMode(v as Mode)} pickerStyle={"segmented"} > automobile walking ))} ) } async function run() { await Navigation.present({ element: }) Script.exit() } run() ``` --- url: /guide/Device Capabilities/AVCaptureSession & Camera Control/Live Photo/index.md --- # Live Photo A Live Photo is a photo _plus_ a short `.mov` companion clip captured around the shutter. `AVCapturePhotoOutput` lets you opt in per capture — pass `livePhotoMovieFile` to `capturePhoto` and `capturePhoto` resolves with both the still image and the path of the saved movie. ## Prerequisites - The photo output must have `isLivePhotoCaptureSupported === true` (true on all camera-capable iPhones). - You must enable it once on the output: `photoOutput.isLivePhotoCaptureEnabled = true`. The bridge silently clamps this to `false` on outputs that don't support Live Photo, so a follow-up read is the simplest feature gate. - If `isLivePhotoCaptureEnabled` is `false` at the moment `capturePhoto` is called with `livePhotoMovieFile`, the promise **rejects immediately** rather than performing a non-Live capture. - Do not enable `isAutoDeferredPhotoDeliveryEnabled` at the same time. With deferred on, the system may finalize without producing the `.mov`, and `livePhotoMovieFileURL` will be absent from the resolved value. ## Wiring it up ```ts const camera = AVCaptureDevice.default("video")! const session = new AVCaptureSession() const photoOutput = new AVCapturePhotoOutput() session.configure(() => { session.sessionPreset = "photo" session.addInput(new AVCaptureDeviceInput(camera)) session.addOutput(photoOutput) }) // One-time setup. The flag survives across capturePhoto calls. photoOutput.isLivePhotoCaptureEnabled = true await session.startRunning() const ts = Date.now() const photoFile = `${FileManager.documentsDirectory}/live_${ts}.heic` const movieFile = `${FileManager.documentsDirectory}/live_${ts}.mov` const result = await photoOutput.capturePhoto({ codec: "hevc", photoFile, // ← required if you'll later call Photos.saveLivePhoto livePhotoMovieFile: movieFile, livePhotoVideoCodec: "hevc", }) console.log("photo:", result.image.width, "×", result.image.height) console.log("still file:", result.photoFileURL) console.log("movie:", result.livePhotoMovieFileURL) ``` The resolved object has the usual `image / metadata / isRawPhoto / isDeferredProxy` plus an extra `photoFileURL: string` (if you passed `photoFile`) and `livePhotoMovieFileURL: string` (if you passed `livePhotoMovieFile`). If you didn't request a Live Photo, the `.mov` field is absent. ### Why `photoFile` matters for Live Photo `capturePhoto` always gives you `result.image: UIImage` — but `UIImage` is a decoded bitmap with no original metadata. When you save a Live Photo to the system Photo Library via `Photos.saveLivePhoto(...)`, PhotoKit verifies that the still and the `.mov` share a Live Photo asset identifier (embedded in the Apple Maker Note under key 17 on the still; in `com.apple.quicktime.content.identifier` on the movie). Re-encoding the `UIImage` via `image.toJPEGData()` strips that identifier and PhotoKit rejects the pair with **`PHPhotosErrorDomain 3302`**. The `photoFile` option avoids this by asking the bridge to write `photo.fileDataRepresentation()` straight to disk — that's the raw bytes the camera produced, with the Maker Note intact. Feed `result.photoFileURL` into `Photos.saveLivePhoto({ imagePath, videoPath })` and pairing works. ## What "resolve" actually waits for A Live Photo capture fires two parallel AVFoundation callbacks: one for the still image and one for the `.mov` file. The bridge waits for **both** before resolving — you always get a fully usable pair, or an error. AVFoundation does not guarantee an order between the two, so don't rely on file timestamps to infer it. The system also runs a "capture finish" sweep at the end. If something goes wrong half-way (e.g. the device drops the `.mov`), the bridge falls back to whatever has been delivered so far rather than hanging the promise. ## File path rules - Pass an **absolute path** ending in `.mov`. `${FileManager.documentsDirectory}/...` is the easiest source of a writable path. - AVFoundation refuses to write to an existing path. The bridge deletes the file at the requested path **before** the capture starts to spare you that error. - The `.mov` is small (\~2–4 MB for a 1.5 s clip). Clean up old files yourself if you're capturing repeatedly. ## Codec selection `livePhotoVideoCodec` is optional. Acceptable values: - `"hevc"` — preferred on iPhone 7 and later (supported devices). Smaller files. - `"h264"` — wider compatibility (older systems, some editing tools). If you pass a codec that the device doesn't list in `availableLivePhotoVideoCodecTypes`, the bridge silently lets AVFoundation pick its default rather than failing the capture. Read the resolved file with an `AVAsset` if you need to know what was actually written. ## Things to know - Live Photo + `flashMode = "on"` works, but the captured movie includes the flash flicker; design for that. - The `.mov` you get is exactly what Photos.app would save — it includes a slight pre-roll and post-roll around the shutter. - `capturePhoto` only writes files to disk — it doesn't put anything in the system Photo Library. To save the photo and clip as a single linked Live Photo (the way Camera.app does it), pass both paths to [`Photos.saveLivePhoto`](#): ```ts await Photos.saveLivePhoto({ imagePath: result.photoFileURL!, // raw HEIC from `photoFile` option videoPath: result.livePhotoMovieFileURL!, // .mov from `livePhotoMovieFile` option shouldMoveFile: true, // move both into Photos rather than copy }) ``` This pairs them into one PHAsset; Photos.app shows the long-press "Live" animation on it. The first call triggers the system Photo Library permission prompt. **Do not** substitute `result.image.toJPEGData()` for `result.photoFileURL` — the re-encoded JPEG loses the Live Photo asset identifier and PhotoKit rejects the pairing. --- url: /guide/Device Capabilities/AVCaptureSession & Camera Control/Live Photo/index_example.md --- # Example ```tsx import { Button, CaptureVideoPreviewView, HStack, Image, Navigation, NavigationStack, Script, Spacer, Text, Toolbar, ToolbarItem, useEffect, useMemo, useObservable, VStack } from "scripting" type Shot = { index: number image: UIImage photoSize: string /** native 写盘的原始 still bytes(HEIC/JPEG),含 Live Photo asset identifier */ photoURL?: string movieURL?: string movieSize?: number ms: number isDeferredProxy: boolean } function View() { const dismiss = Navigation.useDismiss() const isRunning = useObservable(false) const shots = useObservable([]) const captureCount = useObservable(0) const lastError = useObservable("") const livePhotoSupp = useObservable(false) const livePhotoEn = useObservable(false) const { session, camera, photoOutput } = useMemo(() => { const camera = AVCaptureDevice.default("video")! const session = new AVCaptureSession() const input = new AVCaptureDeviceInput(camera) const photoOutput = new AVCapturePhotoOutput() session.configure(() => { session.sessionPreset = "photo" if (session.canAddInput(input)) session.addInput(input) if (session.canAddOutput(photoOutput)) session.addOutput(photoOutput) }) // 必须在 addOutput 之后查 supported,在那之前 photoOutput 没绑 session 时 supported=false。 return { session, camera, photoOutput } }, []) useEffect(() => { async function start() { try { await session.startRunning() isRunning.setValue(true) // Live Photo 必须开启 enabled 之后才会真正生效。 photoOutput.isLivePhotoCaptureEnabled = true // 读回真实值: 不支持的设备 setter 会被 clamp 回 false。 livePhotoSupp.setValue(photoOutput.isLivePhotoCaptureSupported) livePhotoEn.setValue(photoOutput.isLivePhotoCaptureEnabled) console.log("[startRunning]", { preset: session.sessionPreset, liveSupp: photoOutput.isLivePhotoCaptureSupported, liveEn: photoOutput.isLivePhotoCaptureEnabled, }) } catch (e) { await Dialog.alert({ message: `Failed to start: ${String(e)}` }) dismiss() } } start() return () => { session.stopRunning().finally(() => session.dispose()) } }, []) async function takeLivePhoto() { const idx = captureCount.value + 1 captureCount.setValue(idx) const ts = Date.now() // 同时让 native 写盘 still + .mov。两份文件共享 Apple Maker Note 里的 // Live Photo asset identifier, Photos.saveLivePhoto 才能 pair 上。 const photoFile = `${FileManager.documentsDirectory}/live_${idx}_${ts}.heic` const movieFile = `${FileManager.documentsDirectory}/live_${idx}_${ts}.mov` const start = Date.now() try { const result = await photoOutput.capturePhoto({ codec: "hevc", photoFile, livePhotoMovieFile: movieFile, livePhotoVideoCodec: "hevc", }) const ms = Date.now() - start // 读 movie 文件大小供 UI 显示 let movieSize: number | undefined if (result.livePhotoMovieFileURL) { try { const stat = await FileManager.stat(result.livePhotoMovieFileURL) movieSize = stat.size } catch { /* ignore */ } } shots.setValue([{ index: idx, image: result.image, photoSize: `${result.image.width}×${result.image.height}`, photoURL: result.photoFileURL, movieURL: result.livePhotoMovieFileURL, movieSize, ms, isDeferredProxy: result.isDeferredProxy, }, ...shots.value].slice(0, 6)) lastError.setValue("") } catch (e) { lastError.setValue(String(e)) } } /** 不开 Live Photo,验证退化路径:resolve 不应包含 livePhotoMovieFileURL */ async function takeRegular() { const idx = captureCount.value + 1 captureCount.setValue(idx) const start = Date.now() try { const result = await photoOutput.capturePhoto({ codec: "hevc" }) shots.setValue([{ index: idx, image: result.image, photoSize: `${result.image.width}×${result.image.height}`, movieURL: result.livePhotoMovieFileURL, // 应为 undefined ms: Date.now() - start, isDeferredProxy: result.isDeferredProxy, }, ...shots.value].slice(0, 6)) lastError.setValue("") } catch (e) { lastError.setValue(String(e)) } } /** * 把最新一张 Live Photo 配对存进系统照片库。 * 关键: `imagePath` 必须用 native 写盘的 `result.photoFileURL`(原始 HEIC, * 含 Apple Maker Note 里的 Live Photo asset identifier),用 toJPEGData 重编码 * 出来的 JPEG 会丢这个 identifier, Photos.saveLivePhoto 报 PHPhotosError 3302。 * `shouldMoveFile: true` 会把磁盘上两个文件 move 进 Photos,本地副本就消失了。 */ async function saveLast() { const last = shots.value[0] if (!last) { lastError.setValue("Capture a Live Photo first") return } if (!last.photoURL || !last.movieURL) { lastError.setValue("Last capture missing photoURL or movieURL (was it a Regular shot?)") return } try { await Photos.saveLivePhoto({ imagePath: last.photoURL, videoPath: last.movieURL, shouldMoveFile: true, }) lastError.setValue(`Saved Live Photo #${last.index} to Photos library`) } catch (e) { lastError.setValue(`Save failed: ${String(e)}`) console.error(e) } } /** 故意关掉 enabled 再调 Live Photo,期望 promise reject */ async function probeReject() { photoOutput.isLivePhotoCaptureEnabled = false livePhotoEn.setValue(photoOutput.isLivePhotoCaptureEnabled) try { await photoOutput.capturePhoto({ livePhotoMovieFile: `${FileManager.documentsDirectory}/should_reject.mov`, }) lastError.setValue("Unexpected: did not reject") } catch (e) { lastError.setValue(`OK (rejected): ${String(e)}`) } finally { // 恢复 enabled 方便继续测 photoOutput.isLivePhotoCaptureEnabled = true livePhotoEn.setValue(photoOutput.isLivePhotoCaptureEnabled) } } return ( )} } } async function run() { await Navigation.present({ element: }) Script.exit() } run() ``` --- url: /guide/Device Capabilities/Health/Reading Quantity Samples.md --- # Reading Quantity Samples The Scripting app allows you to query **quantity-based health data**, such as step count, heart rate, body mass, calories burned, distance, and more, using the global `Health.queryQuantitySamples()` API. This guide explains how to retrieve quantity samples and work with the results. *** ## What Are Quantity Samples? A **quantity sample** represents a numeric health measurement taken at a specific time or over a time interval. Common examples include: - `stepCount` - `heartRate` - `bodyMass` - `activeEnergyBurned` - `distanceWalkingRunning` These samples can be either **discrete** (a single measurement) or **cumulative** (a value summed over time). *** ## API Overview ```ts Health.queryQuantitySamples( quantityType: HealthQuantityType, options?: { startDate?: Date endDate?: Date limit?: number strictStartDate?: boolean strictEndDate?: boolean sortDescriptors?: Array<{ key: "startDate" | "endDate" | "count" order?: "forward" | "reverse" }> } ): Promise> ``` *** ## Parameters - `quantityType`: The health data type to query (e.g., `"stepCount"`, `"heartRate"`) - `startDate` / `endDate`: Time range for filtering samples - `limit`: Maximum number of results - `strictStartDate` / `strictEndDate`: If `true`, only samples starting/ending exactly at those dates will be included - `sortDescriptors`: Optional array to sort results by `startDate`, `endDate`, or `count` *** ## Sample Code: Reading Step Count ```ts const results = await Health.queryQuantitySamples("stepCount", { startDate: new Date("2025-07-01T00:00:00"), endDate: new Date("2025-07-02T00:00:00"), limit: 10, sortDescriptors: [{ key: "startDate", order: "reverse" }] }) for (const sample of results) { const value = sample.quantityValue(HealthUnit.count()) console.log(`Steps: ${value} from ${sample.startDate} to ${sample.endDate}`) } ``` *** ## Sample Code: Reading Heart Rate with Unit ```ts const results = await Health.queryQuantitySamples("heartRate", { startDate: new Date(Date.now() - 3600 * 1000), // past hour }) for (const sample of results) { const bpm = sample.quantityValue( HealthUnit.count().divided(HealthUnit.minute()) ) console.log(`Heart Rate: ${bpm} bpm at ${sample.startDate}`) } ``` *** ## Interpreting Sample Types Each sample returned may be: - `HealthQuantitySample`: The base class - `HealthCumulativeQuantitySample`: Includes `.sumQuantity(unit)` - `HealthDiscreteQuantitySample`: Includes `.averageQuantity(unit)`, `.maximumQuantity(unit)`, etc. You can use `instanceof` or feature detection to check for extended properties. Example: ```ts if ("averageQuantity" in sample) { const avg = sample.averageQuantity(HealthUnit.count()) console.log("Average:", avg) } ``` *** ## Notes - The unit passed to `.quantityValue()` **must match** the type (e.g., use `count()` for steps, `gram(HealthMetricPrefix.kilo)` for body mass). - Some types (like heart rate) require compound units like `count().divided(minute())`. - The time interval is defined by `startDate` and `endDate` on each sample. - Samples may have an optional `.metadata` and `.count`. *** ## Common Units by Type | Quantity Type | Recommended Unit | | -------------------------- | ------------------------------------------------- | | `"stepCount"` | `HealthUnit.count()` | | `"heartRate"` | `HealthUnit.count().divided(HealthUnit.minute())` | | `"bodyMass"` | `HealthUnit.gram(HealthMetricPrefix.kilo)` | | `"activeEnergyBurned"` | `HealthUnit.kilocalorie()` | | `"distanceWalkingRunning"` | `HealthUnit.meter()` | *** ## Error Handling ```ts try { const results = await Health.queryQuantitySamples("stepCount") console.log("Sample count:", results.length) } catch (err) { console.error("Failed to query samples:", err) } ``` *** ## Summary To read quantity samples: 1. Call `Health.queryQuantitySamples(type, options)` 2. Loop through the results 3. Use `.quantityValue(unit)` or `.sumQuantity(unit)` depending on the type This API gives you powerful access to time-series health data stored in HealthKit. --- url: /guide/Device Capabilities/Health/Reading Workout Samples.md --- # Reading Workout Samples The Scripting app allows you to retrieve **workout sessions** from HealthKit using the global `Health.queryWorkouts()` function. Workouts represent physical activity sessions such as running, walking, swimming, cycling, strength training, and more. Each workout includes metadata such as duration, activity type, start/end times, and detailed statistics like heart rate, distance, and energy burned. *** ## What Is a Workout? A **HealthWorkout** record contains: - `startDate` / `endDate`: The duration of the workout session - `duration`: Total duration in seconds - `workoutActivityType`: Enum representing the workout type (e.g., running, walking) - `metadata`: Optional custom metadata - `workoutEvents`: Optional array of workout-related events (e.g., pauses, laps) - `allStatistics`: A dictionary of detailed quantity statistics (e.g., heart rate, distance, calories) *** ## API Overview ```ts Health.queryWorkouts( options?: { startDate?: Date endDate?: Date limit?: number strictStartDate?: boolean strictEndDate?: boolean sortDescriptors?: Array<{ key: "startDate" | "endDate" | "duration" order?: "forward" | "reverse" }> requestPermissions?: HealthQuantityType[] } ): Promise ``` *** ## Parameters | Parameter | Description | | ----------------------------------- | ----------------------------------------------------------------------------------- | | `startDate` / `endDate` | Optional time range to filter workouts | | `limit` | Maximum number of workouts to return | | `strictStartDate` / `strictEndDate` | Whether to match the exact start/end dates | | `sortDescriptors` | Sort results by `startDate`, `endDate`, or `duration` | | `requestPermissions` | An array of health quantity types for which to request permissions before querying. | *** ## Example: Read Recent Workouts ```ts const workouts = await Health.queryWorkouts({ startDate: new Date("2025-07-01"), endDate: new Date("2025-07-05"), sortDescriptors: [{ key: "startDate", order: "reverse" }] }) for (const workout of workouts) { console.log("Workout Type:", workout.workoutActivityType) console.log("Start:", workout.startDate) console.log("End:", workout.endDate) console.log("Duration (min):", workout.duration / 60) const heartRate = workout.allStatistics["heartRate"] const energy = workout.allStatistics["activeEnergyBurned"] if (heartRate) { const avgHR = heartRate.averageQuantity(HealthUnit.count().divided(HealthUnit.minute())) console.log("Avg Heart Rate:", avgHR) } if (energy) { const kcal = energy.sumQuantity(HealthUnit.kilocalorie()) console.log("Calories Burned:", kcal) } console.log("---") } ``` *** ## Accessing Detailed Statistics The `allStatistics` dictionary provides detailed quantity data recorded during the workout. You can extract values using: ```ts const stat = workout.allStatistics["heartRate"] const avg = stat?.averageQuantity(HealthUnit.count().divided(HealthUnit.minute())) const max = stat?.maximumQuantity(HealthUnit.count().divided(HealthUnit.minute())) ``` Common available statistics include: - `"heartRate"` - `"activeEnergyBurned"` - `"distanceWalkingRunning"` - `"stepCount"` *** ## Workout Events (Optional) If recorded, `workout.workoutEvents` contains an array of time-stamped workout events: ```ts for (const event of workout.workoutEvents || []) { console.log("Event:", event.type) console.log("From:", event.dateInterval.start) console.log("To:", event.dateInterval.end) } ``` Event types include: pause, resume, lap, segment, motion pause/resume, etc. *** ## Notes - Each workout is an instance of `HealthWorkout` - `workoutActivityType` is an enum, which you can map to labels or icons - If `allStatistics` is missing some keys, the data may not have been recorded by the device/app - You can combine workout data with category and quantity samples for full activity insights *** ## Summary To read workouts from HealthKit: 1. Call `Health.queryWorkouts(options)` to get a list of `HealthWorkout` records 2. Use `startDate`, `endDate`, and sorting options to filter and order results 3. Access properties like `duration`, `activityType`, and `allStatistics` for insights 4. Optionally inspect `workoutEvents` and metadata This API is ideal for analyzing exercise history, generating workout summaries, or visualizing fitness trends. --- url: /guide/Device Capabilities/Health/Requesting Multiple Permissions at Once/index.md --- # Requesting Multiple Permissions at Once When a script needs access to several kinds of HealthKit data, you don't have to ask for each permission separately. If you trigger multiple permission-requiring `Health` APIs around the same time, the app collects the pending authorizations within a short window and presents a **single** HealthKit authorization sheet that lists all of them. This keeps the experience clean: the user sees one combined sheet instead of a chain of separate prompts. *** ## How It Works Every read/write `Health` API (such as `queryQuantitySamples`, `queryCategorySamples`, `queryWorkouts`, or `dateOfBirth`) requests authorization for the data it touches before running. When you start several of them together, those requests are merged: - Requests that arrive close together are batched into one authorization sheet. - Data types that were already authorized are skipped, so no sheet appears when nothing new is needed. - If more requests arrive while a sheet is still on screen, they are queued and presented after the current one is dismissed. You don't call any explicit "request permission" method — just call the APIs you need, and authorization is requested automatically. *** ## Requesting Multiple Permissions at Once Use `Promise.all` (or `Promise.allSettled`) to start the queries together: ```ts const results = await Promise.allSettled([ Health.queryQuantitySamples("stepCount", { limit: 1 }), Health.queryQuantitySamples("heartRate", { limit: 1 }), Health.queryCategorySamples("sleepAnalysis", { limit: 1 }), Health.queryWorkouts({ limit: 1 }), Health.dateOfBirth(), ]) console.log(results) ``` All of these queries touch different HealthKit data types, so HealthKit shows one sheet covering every type that still needs authorization. *** ## Requesting One at a Time If you `await` each request before starting the next, every call completes its authorization before the following one begins, so the user may see a separate sheet for each: ```ts await Health.queryQuantitySamples("stepCount", { limit: 1 }) await Health.queryCategorySamples("sleepAnalysis", { limit: 1 }) await Health.queryWorkouts({ limit: 1 }) ``` Prefer starting requests together when you know up front which data types your script needs. *** ## Notes - `Health` APIs require a Scripting PRO subscription. - Check `Health.isHealthDataAvailable` before querying; it is `false` on devices without HealthKit. - The authorization sheet only lists data types that are not yet determined. If everything is already authorized, no sheet appears and the queries run immediately. --- url: /guide/Device Capabilities/Health/Requesting Multiple Permissions at Once/index_example.md --- # Example ```tsx import { Button, List, Navigation, NavigationStack, Script, Text } from "scripting" // 同时触发多个需要授权的 Health API,验证它们会合并成一次 HealthKit 授权弹框。 async function requestTogether() { if (!Health.isHealthDataAvailable) { await Dialog.alert({ message: "Health data is not available on this device." }) return } // Fire several permission-requiring queries at the same time. The app // collects the pending authorizations within a short window and shows a // single HealthKit permission sheet covering all of them. const tasks: Record> = { "Step Count": Health.queryQuantitySamples("stepCount", { limit: 1 }), "Heart Rate": Health.queryQuantitySamples("heartRate", { limit: 1 }), "Active Energy": Health.queryQuantitySamples("activeEnergyBurned", { limit: 1 }), "Sleep Analysis": Health.queryCategorySamples("sleepAnalysis", { limit: 1 }), "Workouts": Health.queryWorkouts({ limit: 1 }), "Date of Birth": Health.dateOfBirth(), } const labels = Object.keys(tasks) const results = await Promise.allSettled(Object.values(tasks)) const summary = results.map((result, index) => { const label = labels[index] if (result.status === "fulfilled") { const value = result.value const detail = Array.isArray(value) ? `${value.length} item(s)` : "ok" return `${label}: ${detail}` } return `${label}: failed (${result.reason})` }).join("\n") console.log(summary) await Dialog.alert({ title: "Permission results", message: summary }) } // 逐个 await 调用:每个请求在下一个开始前就完成授权,因此会弹出多个分开的授权弹框, // 与上面的"合并请求"形成对照。 async function requestSequentially() { if (!Health.isHealthDataAvailable) { await Dialog.alert({ message: "Health data is not available on this device." }) return } try { await Health.queryQuantitySamples("stepCount", { limit: 1 }) await Health.queryCategorySamples("sleepAnalysis", { limit: 1 }) await Health.queryWorkouts({ limit: 1 }) await Dialog.alert({ message: "Sequential requests finished." }) } catch (error) { await Dialog.alert({ title: "Failed", message: String(error) }) } } function Example() { return Calling several permission-requiring Health APIs at once merges their authorization into a single HealthKit sheet. })} {resp != null ? <> : <> } {/* 未选中的先画(细),选中的最后画压在上面(粗)。MapPolyline 目前不接收 opacity / 视图层 modifier,这里用 stroke width(3 vs 6)制造主次对比。 */} {routes.map((r, i) => { if (i === selectedIdx) return null return })} {routes[selectedIdx] != null ? : null} } // ─────────────────────────────────────────────────────────────────────── // Demo 4: calculateETA — time / distance only, no geometry download // ─────────────────────────────────────────────────────────────────────── function ETADemo() { type Mode = "automobile" | "walking" const [mode, setMode] = useState("automobile") const [eta, setEta] = useState(null) const [loading, setLoading] = useState(false) const [err, setErr] = useState(null) const calc = async () => { setLoading(true); setErr(null) try { const result = await MapDirections.calculateETA({ source: { coordinate: PEOPLES_SQUARE, name: "People's Square" }, destination: { coordinate: LUJIAZUI, name: "Lujiazui" }, transportType: mode, departureDate: new Date(), }) setEta(result) } catch (e) { setErr(String(e)) } finally { setLoading(false) } } return 4. `calculateETA` Skip the polyline download; get back travel time / distance / arrival window only. Cheaper than `calculate` when the UI just needs a headline number. setMode(v as Mode)} pickerStyle={"segmented"} > automobile walking `) await webView.present({ navigationTitle: 'WebView Demo' }) webView.dispose() ``` --- url: /guide/Intent/Intent.continueInForeground.md --- # Intent.continueInForeground `Intent.continueInForeground` is an API that leverages the **iOS 26+ AppIntents framework** to request the system to bring the **Scripting app** to the foreground while a Shortcut is running. This method is used when a script—invoked from Shortcuts—requires full UI interaction within the Scripting app (for example: presenting a form, editing content, picking files, showing a full screen navigation flow, etc.). When invoked: - The system displays a dialog asking the user to continue the workflow in the app. - If the user **confirms**, the system opens Scripting in the foreground and the script continues. - If the user **cancels**, the script terminates immediately. Because this is a system-level capability of AppIntents: **This API requires iOS 26 or later.** *** ## API Definition ```ts function continueInForeground( dialog?: Dialog | null, options?: { alwaysConfirm?: boolean; } ): Promise; ``` ## Parameters ### `dialog?: Dialog | null` An optional message explaining why the workflow needs to continue in the foreground. `Dialog` supports four formats: ```ts type Dialog = | string | { full: string; supporting: string } | { full: string; supporting: string; systemImageName: string } | { full: string; systemImageName: string } ``` Examples: ```ts "Do you want to continue in the app?" ``` ```ts { full: "Continue in the Scripting app?", supporting: "The next step requires full UI interaction.", systemImageName: "app" } ``` Passing `null` will suppress the dialog entirely (not recommended unless you fully understand the UX implications). *** ### `options?: { alwaysConfirm?: boolean }` Controls whether the system should always ask for confirmation: - `alwaysConfirm: false` _(default)_ The system may decide whether confirmation is needed based on context. - `alwaysConfirm: true` The system always presents the confirmation dialog. *** ## Execution Behavior When called inside `intent.tsx`: 1. The Shortcut pauses execution. 2. The system presents a confirmation dialog. 3. If the user accepts: - The Scripting app opens in the foreground. - The script continues executing after the `await`. 4. If the user cancels: - The entire script is terminated immediately. This mirrors the behavior of Apple’s AppIntents `continueInApp()` functionality for system apps. *** ## Common Use Cases Use `continueInForeground` when the next step **cannot** run in the background, including: - Presenting a full-screen UI (`Navigation.present`) - Editing content in a custom form or navigation stack - Selecting files or interacting with UI components - Scenarios requiring user input or multi-step flows - Showing UI unavailable to background extensions It should **not** be used for simple data processing or non-interactive tasks. *** ## Full Code Example Below is the full working example demonstrating how `continueInForeground` enables a Shortcut to transfer execution into the Scripting app and then return UI input back to Shortcuts. ```tsx // intent.tsx import { Button, Intent, List, Navigation, NavigationStack, Script, Section, TextField, useState } from "scripting" function View() { const dismiss = Navigation.useDismiss() const [text, setText] = useState("") return

} async function runIntent() { // Step 1: Ask the user to continue in the foreground app await Intent.continueInForeground( "Do you want to open the app and continue?" ) // Step 2: Present UI inside the Scripting app const text = await Navigation.present( ) // Step 3: Optionally go back to Shortcuts Safari.openURL("shortcuts://") // Step 4: Return the result to Shortcuts Script.exit( Intent.text( text ?? "No text return" ) ) } runIntent() ``` *** ## Notes and Recommendations 1. **Requires iOS 26+** Do not call this API on older systems. 2. **Use dialogs to explain why foreground interaction is required** This improves user trust and Shortcuts clarity. 3. **Always handle the cancellation case** If the user cancels, your script stops. Avoid assuming foreground UI will always appear. 4. **Foreground UI must be meaningful** Only use this API when the upcoming step truly requires UI. 5. **Can be combined with SnippetIntent (iOS 26+)** For workflows that mix in-Shortcut Snippet UI with in-app full UI. *** ## Summary `Intent.continueInForeground` enables scripts invoked from Shortcuts to request foreground execution when UI interaction is required. It is: - Based on iOS 26 AppIntents capabilities - A system-confirmed context switch - Essential for workflows involving full UI interactions - Safely integrated via a structured `Dialog` system This method allows Scripting to support advanced automation flows that seamlessly transition between Shortcuts and the full Scripting app UI. --- url: /guide/Intent/Intent.requestConfirmation.md --- # Intent.requestConfirmation `Intent.requestConfirmation` pauses script execution and asks the user to confirm an action through a **system-managed confirmation UI**. The confirmation interface consists of: - A **SnippetIntent UI** (provided by you) - Optional dialog text (system-generated or developer-defined) Behavior: - If the user **confirms**, the script continues (Promise resolves). - If the user **cancels**, the script terminates immediately. - The UI is fully managed by the system. - The presented UI is defined by the provided SnippetIntent’s `perform()` return. **This API is only available on iOS 26 or later.** *** ## API Definition ```ts Intent.requestConfirmation( actionName: ConfirmationActionName, snippetIntent: AppIntent, options?: { dialog?: Dialog; showDialogAsPrompt?: boolean; } ): Promise ``` *** ## Parameter Details ## actionName: ConfirmationActionName A semantic keyword describing the type of action being confirmed. Apple uses this value to generate natural language around the confirmation UI. Accepted values include: ``` "add" | "addData" | "book" | "buy" | "call" | "checkIn" | "continue" | "create" | "do" | "download" | "filter" | "find" | "get" | "go" | "log" | "open" | "order" | "pay" | "play" | "playSound" | "post" | "request" | "run" | "search" | "send" | "set" | "share" | "start" | "startNavigation" | "toggle" | "turnOff" | "turnOn" | "view" ``` Examples: - `"set"` → “Do you want to set...?” - `"buy"` → “Do you want to buy...?” - `"toggle"` → “Do you want to toggle...?” Choosing the correct semantic verb improves the clarity of the user-facing dialog. *** ## snippetIntent: SnippetIntent This must be an AppIntent registered with: ```ts protocol: AppIntentProtocol.SnippetIntent; ``` The UI displayed in the confirmation step **comes from this SnippetIntent’s `perform()` return**, which must be a TSX-based `VirtualNode`. This is what the user sees and interacts with during confirmation. *** ## `options?: { dialog?: Dialog; showDialogAsPrompt?: boolean }` ### dialog?: Dialog Optional text describing the confirmation request. Supports four formats: ```ts type Dialog = | string | { full: string; supporting: string } | { full: string; supporting: string; systemImageName: string } | { full: string; systemImageName: string }; ``` Examples: ```ts "Are you sure you want to continue?"; ``` More structured version: ```ts { full: "Set this color?", supporting: "This will update the theme color used across the app.", systemImageName: "paintpalette" } ``` Use this to clearly explain what the user is confirming. *** ### showDialogAsPrompt?: boolean - Default: `true` The system shows the dialog as a modal prompt. - `false` The dialog may be integrated directly inside the Snippet card instead of a separate prompt. *** ## Execution Flow When the script executes: ```ts await Intent.requestConfirmation(...) ``` The following occurs: 1. Script execution is paused. 2. The system displays: - The SnippetIntent UI - Optional dialog text 3. The user chooses: - **Confirm** → Promise resolves → script continues - **Cancel** → script stops immediately 4. The system handles UI presentation and dismissal automatically. There is no need to manually manage the UI lifecycle. *** ## Usage Scenarios Recommended for: - Confirming important changes (colors, appearance, configurations) - Confirming destructive or irreversible actions - Steps requiring explicit user approval - Initiating subflows requiring UI preview or choice (e.g., color picker, item selector) - Sensitive operations (e.g., updating settings, performing actions with side effects) Not recommended for: - Actions that do not require user approval - Simple background data processing *** ## Complete Example Below is a full working example demonstrating how to request user confirmation using a SnippetIntent. It assumes you have two SnippetIntent AppIntents: - `PickColorIntent` — allows user to select a color - `ShowResultIntent` — displays the final result ## intent.tsx ```tsx import { Intent, Script } from "scripting"; import { PickColorIntent, ShowResultIntent } from "./app_intents"; async function runIntent() { // Step 1: Ask the user to confirm the action via a Snippet UI await Intent.requestConfirmation("set", PickColorIntent(), { dialog: { full: "Are you sure you want to set this color?", supporting: "This will update the theme color used by your app.", systemImageName: "paintpalette", }, }); // Step 2: Read input from Shortcuts const text = Intent.shortcutParameter?.type === "text" ? Intent.shortcutParameter.value : "No text parameter from Shortcuts"; // Step 3: Return another SnippetIntent result const snippet = Intent.snippetIntent({ snippetIntent: ShowResultIntent({ content: text }), }); Script.exit(snippet); } runIntent(); ``` *** ## Notes & Best Practices - **Requires iOS 26+** — do not call this API on earlier versions. - Always include a clear **dialog** message to improve user understanding. - Use for actions that require explicit approval or confirmation. - When possible, combine with SnippetIntent to provide a richer preview UI. - Scripts terminate automatically when the user cancels; do not rely on cleanup code afterward. - Avoid calling it unnecessarily; only use when confirmation is truly meaningful. --- url: /guide/Intent/Quick Start.md --- # Quick Start Scripting allows you to define custom iOS Intents using an `intent.tsx` file. These scripts can receive input from the iOS share sheet or the Shortcuts app and return structured results. With optional UI presentation, you can create interactive workflows that process data and deliver output dynamically. *** ## 1. Creating and Configuring an Intent ### 1.1 Create an Intent Script 1. Create a new script project in the Scripting app. 2. Add a file named `intent.tsx` to the project. 3. Define your logic and optionally a UI component inside the file. ### 1.2 Configure Supported Input Types Tap the project title in the editor’s title bar to open **Intent Settings**, then select supported input types: - Text - Images - File URLs - URLs This configuration enables your script to appear in the share sheet or Shortcuts when matching input is provided. *** ## 2. Accessing Input Data Inside `intent.tsx`, use the `Intent` API to access input values. | Property | Description | | -------------------------- | ----------------------------------------------------------------------------------- | | `Intent.shortcutParameter` | A single parameter passed from the Shortcuts app, with `.type` and `.value` fields. | | `Intent.textsParameter` | Array of text strings. | | `Intent.urlsParameter` | Array of URL strings. | | `Intent.imagesParameter` | Array of image file paths (UIImage objects). | | `Intent.fileURLsParameter` | Array of local file URL paths. | Example: ```ts if (Intent.shortcutParameter) { if (Intent.shortcutParameter.type === "text") { console.log(Intent.shortcutParameter.value) } } ``` *** ## 3. Returning a Result Use `Script.exit(result)` to return a result to the caller, such as the Shortcuts app or another script. Valid return types include: - Plain text: `Intent.text(value)` - Attributed text: `Intent.attributedText(value)` - URL: `Intent.url(value)` - JSON: `Intent.json(value)` - File path or file URL: `Intent.file(value)` or `Intent.fileURL(value)` Example: ```ts import { Script, Intent } from "scripting" Script.exit(Intent.text("Done")) ``` *** ## 4. Displaying Interactive UI Use `Navigation.present()` to show a UI before returning a result. You can render a React-style component and then call `Script.exit()` after the interaction completes. Example: ```ts import { Intent, Script, Navigation, VStack, Text } from "scripting" function MyIntentView() { return ( {Intent.textsParameter?.[0]} ) } async function run() { await Navigation.present({ element: }) Script.exit() } run() ``` *** ## 5. Using Intents in the Share Sheet If a script supports a specific input type (e.g., text or image), it will automatically appear as an option in the iOS share sheet: 1. Select content such as text or a file. 2. Tap the Share button. 3. Choose **Scripting** in the share sheet. 4. Scripting will list scripts that support the selected input type. *** ## 6. Using Intents in the Shortcuts App You can call scripts from the Shortcuts app with or without UI: - **Run Script**: Executes the script in the background. - **Run Script in App**: Executes the script in the foreground, with UI presentation support. Steps: 1. Open the Shortcuts app and create a new shortcut. 2. Add the **Run Script** or **Run Script in App** action from Scripting. 3. Choose the target script and pass input parameters if needed. *** ## 7. Intent API Reference ### `Intent` Properties | Property | Type | Description | | ------------------- | ------------------- | ----------------------------------------------- | | `shortcutParameter` | `ShortcutParameter` | Input from Shortcuts with `.type` and `.value`. | | `textsParameter` | `string[]` | Array of input text values. | | `urlsParameter` | `string[]` | Array of input URLs. | | `imagesParameter` | `UIImage[]` | Array of image file paths or objects. | | `fileURLsParameter` | `string[]` | Array of input file paths (local file URLs). | ### `Intent` Methods | Method | Return Type | Example | | ------------------------------ | --------------------------- | -------------------------------------- | | `Intent.text(value)` | `IntentTextValue` | `Intent.text("Hello")` | | `Intent.attributedText(value)` | `IntentAttributedTextValue` | `Intent.attributedText("Styled Text")` | | `Intent.url(value)` | `IntentURLValue` | `Intent.url("https://example.com")` | | `Intent.json(value)` | `IntentJsonValue` | `Intent.json({ key: "value" })` | | `Intent.file(path)` | `IntentFileValue` | `Intent.file("/path/to/file.txt")` | | `Intent.fileURL(path)` | `IntentFileURLValue` | `Intent.fileURL("/path/to/file.pdf")` | | `Intent.image(UIImage)` | `IntentImageValue` | `Intent.image(uiImage)` | | `Intent.view(node, value?)` | `IntentViewValue` | `Intent.view()` | *** ## 8. Best Practices and Notes - Always call `Script.exit()` to properly terminate the script and return a result. - When displaying a UI, ensure `Navigation.present()` is awaited before calling `Script.exit()`. - Use **"Run Script in App"** for large files or images to avoid process termination due to memory constraints. - You can use `queryParameters` when launching scripts via URL scheme if additional data is needed. --- url: /guide/Intent/SnippetIntent.md --- # SnippetIntent SnippetIntent is a special kind of AppIntent whose purpose is to render **interactive Snippet UI cards** inside the Shortcuts app (iOS 26+). Key characteristics: 1. Must be registered in `app_intents.tsx` 2. Must specify `protocol: AppIntentProtocol.SnippetIntent` 3. `perform()` **must return a VirtualNode (TSX UI)** 4. Must be returned via `Intent.snippetIntent()` 5. Must be invoked from the Shortcuts action **“Show Snippet Intent”** 6. SnippetIntent is ideal for building interactive, step-based UI inside a Shortcut It is not a data-returning Intent; it is exclusively for UI rendering in Shortcuts. *** ## 2. System Requirements **SnippetIntent requires iOS 26 or later.** On iOS versions earlier than 26: - `Intent.snippetIntent()` is not available - `Intent.requestConfirmation()` cannot be used - The Shortcuts action “Show Snippet Intent” does not exist - SnippetIntent-type AppIntents cannot be invoked by Shortcuts *** ## 3. Registering a SnippetIntent (app\_intents.tsx) Example: ```tsx export const PickColorIntent = AppIntentManager.register({ name: "PickColorIntent", protocol: AppIntentProtocol.SnippetIntent, perform: async () => { return } }) ``` Another SnippetIntent: ```tsx export const ShowResultIntent = AppIntentManager.register({ name: "ShowResultIntent", protocol: AppIntentProtocol.SnippetIntent, perform: async ({ content }: { content: string }) => { return } }) ``` Requirements: - `protocol` **must** be `AppIntentProtocol.SnippetIntent` - `perform()` **must** return a TSX UI (VirtualNode) - SnippetIntent cannot return non-UI types such as text, numbers, JSON, or file paths *** ## 4. Wrapping SnippetIntent Return Values — `Intent.snippetIntent` A SnippetIntent cannot be passed directly to `Script.exit()`. It must be wrapped in a `IntentSnippetIntentValue`. ```tsx const snippetValue = Intent.snippetIntent( ShowResultIntent({ content: "Example Text" }) ) Script.exit(snippetValue) ``` ### Type Definition ```ts type SnippetIntentValue = { value?: IntentAttributedTextValue | IntentFileURLValue | IntentJsonValue | IntentTextValue | IntentURLValue | IntentFileValue | null snippetIntent: AppIntent } declare class IntentSnippetIntentValue extends IntentValue< 'SnippetIntent', SnippetIntentValue > { value: SnippetIntentValue type: 'SnippetIntent' } ``` This wrapper makes the return value compatible with the Shortcuts “Show Snippet Intent” action. *** ## 5. Snippet Confirmation UI — `Intent.requestConfirmation` iOS 26 Snippet Framework provides built-in confirmation UI driven by SnippetIntent. ### API ```ts Intent.requestConfirmation( actionName: ConfirmationActionName, intent: AppIntent, options?: { dialog?: Dialog; showDialogAsPrompt?: boolean; } ): Promise ``` ### ConfirmationActionName A predefined list of semantic action names used by system UI: ``` "add" | "addData" | "book" | "buy" | "call" | "checkIn" | "continue" | "create" | "do" | "download" | "filter" | "find" | "get" | "go" | "log" | "open" | "order" | "pay" | "play" | "playSound" | "post" | "request" | "run" | "search" | "send" | "set" | "share" | "start" | "startNavigation" | "toggle" | "turnOff" | "turnOn" | "view" ``` ### Example ```tsx await Intent.requestConfirmation( "set", PickColorIntent() ) ``` Execution behavior: - Displays a Snippet UI for confirmation - If the user confirms → Promise resolves and script continues - If the user cancels → execution stops (system-driven behavior) *** ## 6. The “Show Snippet Intent” Action in Shortcuts (iOS 26+) iOS 26 adds a new Shortcuts action: **Show Snippet Intent** This action is the only correct way to display SnippetIntent UI. ### Comparison with Other Scripting Actions | Shortcuts Action | UI Shown | Supports SnippetIntent | Usage | | ----------------------------- | ------------------------------ | ---------------------- | ------------------- | | Run Script | None | No | Background logic | | Run Script in App | Fullscreen UI inside Scripting | No | Rich app-level UI | | Show Snippet Intent (iOS 26+) | Snippet card UI | Yes | SnippetIntent flows | ### Usage 1. Add “Show Snippet Intent” in Shortcuts 2. Select a Scripting script project 3. The script must return `Intent.snippetIntent(...)` 4. Shortcuts renders the UI in a Snippet card *** ## 7. IntentMemoryStorage — Cross-Intent State Store ## Why It Exists Every AppIntent execution runs in an isolated environment: - After an AppIntent `perform()` completes → its execution context is destroyed - After a script calls `Script.exit()` → the JS context is destroyed This means local variables **cannot persist between AppIntent calls**. Snippet flows commonly involve: PickColor → SetColor → ShowResult Therefore a cross-Intent state mechanism is required. *** ## IntentMemoryStorage API ```ts namespace IntentMemoryStorage { function get(key: string): T | null function set(key: string, value: any): void function remove(key: string): void function contains(key: string): boolean function clear(): void function keys(): string[] } ``` ### Purpose - Store small pieces of shared data across multiple AppIntents - Works during the entire Shortcut flow - Ideal for selections, temporary configuration, or intent-to-intent handoff ### Example ```ts IntentMemoryStorage.set("color", "systemBlue") const color = IntentMemoryStorage.get("color") ``` ### Guidelines Not recommended for large data. For large data: - Use `Storage` (persistent key-value store) - Or save files via `FileManager` in `appGroupDocumentsDirectory` IntentMemoryStorage should be treated as **temporary, lightweight state**. *** ## 8. Full Example Combining All Features (iOS 26+) ## app\_intents.tsx ```tsx export const SetColorIntent = AppIntentManager.register({ name: "SetColorIntent", protocol: AppIntentProtocol.AppIntent, perform: async (color: Color) => { IntentMemoryStorage.set("color", color) } }) export const PickColorIntent = AppIntentManager.register({ name: "PickColorIntent", protocol: AppIntentProtocol.SnippetIntent, perform: async () => { return } }) export const ShowResultIntent = AppIntentManager.register({ name: "ShowResultIntent", protocol: AppIntentProtocol.SnippetIntent, perform: async ({ content }: { content: string }) => { const color = IntentMemoryStorage.get("color") ?? "systemBlue" return } }) ``` ## intent.tsx ```tsx async function runIntent() { // 1. Ask the user to confirm setting the color via Snippet await Intent.requestConfirmation( "set", PickColorIntent() ) // 2. Read Shortcuts input const textContent = Intent.shortcutParameter?.type === "text" ? Intent.shortcutParameter.value : "No text parameter from Shortcuts" // 3. Create final SnippetIntent UI const snippetIntentValue = Intent.snippetIntent({ snippetIntent: ShowResultIntent({ content: textContent }) }) Script.exit(snippetIntentValue) } runIntent() ``` ## Shortcuts Flow 1. User provides text 2. “Show Snippet Intent” runs the script 3. Script displays PickColorIntent confirmation UI via requestConfirmation 4. After confirmation, displays ShowResultIntent Snippet UI 5. Uses IntentMemoryStorage to persist the selected color *** ## 9. Summary This document introduces all **new** Scripting features added for iOS 26+: 1. **SnippetIntent** - Registered using `AppIntentManager` - Returns TSX UI - Requires iOS 26+ 2. **Intent.snippetIntent** - Wraps a SnippetIntent for Script.exit 3. **Intent.requestConfirmation** - Presents a confirmation Snippet UI - Requires SnippetIntent 4. **“Show Snippet Intent” action in Shortcuts** - Required to display SnippetIntent UI 5. **IntentMemoryStorage** - Lightweight cross-AppIntent storage - Not suitable for large binary/content data - Complements multi-step Snippet flows --- url: /guide/Interactive Widget and LiveActivity.md --- # Interactive Widget and LiveActivity The **Scripting** app supports adding interactivity to **widgets** and **LiveActivity**, allowing you to create dynamic and interactive UIs using `Button` and `Toggle` components. These controls can execute **AppIntents** to trigger actions, making your widgets and live activities more powerful. *** ## 1. Introduction to AppIntents ### What are AppIntents? An **AppIntent** defines a specific action that can be triggered by a control (e.g., a `Button` or `Toggle`) in a widget or LiveActivity UI. AppIntents enable seamless interaction and functionality by linking UI components with executable logic. ### Supported Protocols AppIntents can implement the following protocols: - **`AppIntent`**: General-purpose intents for triggering custom actions. - **`AudioPlaybackIntent`**: Handles audio playback (e.g., play, pause, or toggle audio states). - **`AudioRecordingIntent`**: Manages audio recording states (requires iOS 18+ and a LiveActivity to stay active during recording). - **`LiveActivityIntent`**: Modifies or manages LiveActivity states. *** ## 2. Registering an AppIntent To use an **AppIntent**, it must first be registered in the `app_intents.tsx` file using the `AppIntentManager.register` method. ### Example: Registering AppIntents ```typescript // app_intents.tsx import { AppIntentManager, AppIntentProtocol } from "scripting" // Register an AppIntent const IntentWithoutParams = AppIntentManager.register({ name: "IntentWithoutParams", protocol: AppIntentProtocol.AppIntent, perform: async (params: undefined) => { // Perform a custom action console.log("Intent triggered") // Optionally reload widgets Widget.reloadAll() } }) // Register an AppIntent with parameters const ToggleIntentWithParams = AppIntentManager.register({ name: "ToggleIntentWithParams", protocol: AppIntentProtocol.AudioPlaybackIntent, perform: async (audioName: string) => { // Perform action based on the parameter console.log(`Toggling audio playback for: ${audioName}`) Widget.reloadAll() } }) ``` *** ## 3. Using AppIntents in Widgets or LiveActivity UIs After registering an AppIntent, it can be linked to interactive components like `Button` and `Toggle` in your `widget.tsx` or LiveActivity UI file. ### Example: Using AppIntents in a Widget ```typescript // widget.tsx import { VStack, Button, Toggle } from "scripting" import { IntentWithoutParams, ToggleIntentWithParams } from "./app_intents" import { model } from "./model" function WidgetView() { return ( ] }} trailingSwipeActions={{ actions: [ , ] }} /> )} } async function run() { await Navigation.present({ element: }) Script.exit() } run() ``` --- url: /guide/View Modifiers/Symbol Style/index.md --- # Symbol Style These modifiers allow you to customize how **SF Symbols** are displayed and animated inside views, particularly with the `Image` component. *** ## `symbolRenderingMode` Sets the **rendering mode** for symbol images within the view. ### Type ```ts symbolRenderingMode?: SymbolRenderingMode ``` ### Options (`SymbolRenderingMode`) - `"monochrome"` – A single-color version using the foreground style - `"hierarchical"` – Multiple layers with different opacities for depth (good for semantic coloring) - `"multicolor"` – Uses the symbol's built-in colors - `"palette"` – Allows layered tinting (like using multiple `foregroundStyle` layers) ### Example ```tsx ``` ### Explanation: - `symbolRenderingMode="palette"` tells the system to render the symbol in **multiple layered styles**. - `foregroundStyle` now uses an object with `primary`, `secondary`, and optionally `tertiary` layers to color those symbol layers individually. > This matches SwiftUI's behavior with `.symbolRenderingMode(.palette)` and `.foregroundStyle(primary, secondary, tertiary)`. *** ## `symbolVariant` Displays the symbol with a particular **visual variant**. ### Type ```ts symbolVariant?: SymbolVariants ``` ### Options (`SymbolVariants`) - `"none"` – Default symbol with no variant - `"circle"` – Encapsulated in a circle - `"square"` – Encapsulated in a square - `"rectangle"` – Encapsulated in a rectangle - `"fill"` – Filled symbol - `"slash"` – Adds a slash over the symbol (often used to indicate "off" states) ### Example ```tsx ``` *** ## `symbolEffect` Applies a **symbol animation effect** to the view. This can include transitions (appear/disappear), scale, bounce, rotation, breathing, pulsing, and wiggle effects. You can also bind the effect to a value so it animates when the value changes. ### Type ```ts symbolEffect?: SymbolEffect ``` There are two forms of usage: *** ### 1. **Simple effects** (transition, scale, etc.) You can directly assign a symbol effect name: #### Examples ```tsx ``` *** ### 2. **Value-bound discrete effects** These effects animate when the associated value changes. #### Type ```ts symbolEffect?: { effect: DiscreteSymbolEffect value: string | number | boolean options?: SymbolEffectOptions } ``` #### Example ```tsx ``` In this example, each time `isFavorited` changes, the bounce animation is triggered. *** ### 3. **Trigger effects** (`isActive`, mirrors SwiftUI `symbolEffect(_:options:isActive:)`) In SwiftUI's trigger form, the **steady state is `isActive = false`** (symbol visible). Flipping `isActive` plays the effect's animation; the direction depends on the effect: | Effect | `isActive=false` (steady) | `isActive=true` (effect engaged) | | ------------- | ------------------------- | ---------------------------------------- | | `appear` | invisible | visible (appears) | | `disappear` | visible | invisible (disappears) | | `scale` | base size | scaled | | **`drawOn`** | **visible** (drawn) | **invisible** (draw-off animation plays) | | **`drawOff`** | **invisible** | **visible** (draw-on animation plays) | Note that `drawOn` / `drawOff` describe the **animation style** (stroke-by-stroke drawing) — not the final state. `.drawOn` behaves like `.disappear` with a draw-style animation; `.drawOff` behaves like `.appear` with a draw-style animation. ```tsx const [hidden, setHidden] = useState(false) ``` ### Button Executing an AppIntent ```tsx