Skip to content
My custom sidebar

创建控件

使用 Widget() 函数定义主屏幕控件。

控件声明

typescript
import { Widget, Text, VStack, HStack, Image, Spacer } from "perry/widget";

Widget({
  kind: "WeatherWidget",
  displayName: "Weather",
  description: "Shows current weather",
  entryFields: {
    temperature: "number",
    condition: "string",
    location: "string",
  },
  render: (entry) =>
    VStack([
      HStack([
        Text(entry.location),
        Spacer(),
        Image("cloud.sun.fill"),
      ]),
      Text(`${entry.temperature}°`),
      Text(entry.condition),
    ]),
});

控件选项

PropertyTypeDescription
kindstringUnique identifier for the widget
displayNamestringName shown in widget gallery
descriptionstringDescription in widget gallery
entryFieldsobjectData fields with types ("string", "number", "boolean", arrays, optionals, objects)
renderfunctionRender function receiving entry data, returns widget tree. Optional 2nd param for family.
configobjectConfigurable parameters the user can edit (see below)
providerfunctionTimeline provider function for dynamic data (see below)
appGroupstringApp group identifier for sharing data with the host app

Entry Fields

Entry fields define the data your widget displays. Each field has a name and type:

typescript
entryFields: {
  title: "string",
  count: "number",
  isActive: "boolean",
}

Array, Optional, and Object Fields

Entry fields support richer types beyond primitives:

typescript
entryFields: {
  items: [{ name: "string", value: "number" }],  // Array of objects
  subtitle: "string?",                             // Optional string
  stats: { wins: "number", losses: "number" },     // Nested object
}

These compile to a Swift TimelineEntry struct:

swift
struct WeatherEntry: TimelineEntry {
    let date: Date
    let temperature: Double
    let condition: String
    let location: String
}

Conditionals in Render

Use ternary expressions for conditional rendering:

typescript
render: (entry) =>
  VStack([
    Text(entry.isActive ? "Active" : "Inactive"),
    entry.count > 0 ? Text(`${entry.count} items`) : Spacer(),
  ]),

Template Literals

Template literals in widget text are compiled to Swift string interpolation:

typescript
Text(`${entry.name}: ${entry.score} points`)
// Compiles to: Text("\(entry.name): \(entry.score) points")

Configuration Parameters

The config field defines user-editable parameters that appear in the widget's edit UI:

typescript
Widget({
  kind: "CityWeather",
  displayName: "City Weather",
  description: "Weather for a chosen city",
  config: {
    city: { type: "string", displayName: "City", default: "New York" },
    units: { type: "enum", displayName: "Units", values: ["Celsius", "Fahrenheit"], default: "Celsius" },
  },
  entryFields: { temperature: "number", condition: "string" },
  render: (entry) => Text(`${entry.temperature}° ${entry.condition}`),
});

Provider Function

The provider field defines a timeline provider that fetches data for the widget:

typescript
Widget({
  kind: "StockWidget",
  displayName: "Stock Price",
  description: "Shows current stock price",
  config: { symbol: { type: "string", displayName: "Symbol", default: "AAPL" } },
  entryFields: { price: "number", change: "string" },
  provider: async (config) => {
    const res = await fetch(`https://api.example.com/stock/${config.symbol}`);
    const data = await res.json();
    return { price: data.price, change: data.change };
  },
  render: (entry) =>
    VStack([
      Text(`$${entry.price}`).font("title"),
      Text(entry.change).color("green"),
    ]),
});

Placeholder Data

When the widget has no data yet (e.g., first load), the provider can return placeholder data by providing a placeholder field:

typescript
Widget({
  kind: "NewsWidget",
  entryFields: { headline: "string", source: "string" },
  placeholder: { headline: "Loading...", source: "---" },
  // ...
});

Family-Specific Rendering

The render function accepts an optional second parameter for the widget family, allowing different layouts per size:

typescript
render: (entry, family) =>
  family === "systemLarge"
    ? VStack([
        Text(entry.title).font("title"),
        ForEach(entry.items, (item) => Text(item.name)),
      ])
    : HStack([
        Image("star.fill"),
        Text(entry.title).font("headline"),
      ]),

Supported families: "systemSmall", "systemMedium", "systemLarge", "accessoryCircular", "accessoryRectangular", "accessoryInline".

App Group

The appGroup field specifies a shared container for data exchange between the host app and the widget:

typescript
Widget({
  kind: "AppDataWidget",
  appGroup: "group.com.example.myapp",
  // ...
});

Multiple Widgets

Define multiple widgets in a single file. They're bundled into a WidgetBundle:

typescript
Widget({
  kind: "SmallWidget",
  // ...
});

Widget({
  kind: "LargeWidget",
  // ...
});

Next Steps

  • Components — Available widget components and modifiers
  • Overview — Widget system overview

MIT License.