All posts
widgetkitwidgetstimeline-providertutorial

WidgetKit tutorial: generate TimelineProviders from TypeScript with Axint

defineWidget() generates Widget structs, TimelineProviders, and lockscreen descriptors. Here's a step-by-step guide.

Axint TeamMonday, April 6, 20267 min read

A working WidgetKit widget requires a Widget struct, a TimelineProvider, an AppIntent for configuration, entry types, and lockscreen setup. For a stock price widget, that's about 120 lines of Swift before you've fetched a single price.

Axint's defineWidget() handles all of it from one TypeScript declaration.

typescript
const stockPrice = defineWidget({
  name: "StockPrice",
  title: "Stock Price",
  description: "Display current stock price and chart",
  supportedFamilies: ["systemSmall", "systemMedium"],
  intent: {
    name: "SelectStock",
    params: {
      symbol: { type: "string", label: "Stock Symbol" },
    },
  },
  timeline: {
    refreshPolicy: "hourly",
    placeholder: {
      type: "systemSmall",
      data: { price: 150.25, change: 2.5, trend: "up" },
    },
  },
});

Run axint compile and you get:

swift
struct StockPriceWidget: Widget {
  let kind: String = "StockPrice"

  var body: some WidgetConfiguration {
    IntentConfiguration(
      kind: kind,
      intent: SelectStockIntent.self,
      provider: StockPriceTimelineProvider()
    ) { entry in
      StockPriceWidgetEntryView(entry: entry)
    }
    .configurationDisplayName("Stock Price")
    .description("Display current stock price and chart")
    .supportedFamilies([.systemSmall, .systemMedium])
  }
}

Plus a complete AppIntentTimelineProvider with placeholder(), getSnapshot(), and timeline() stubs, along with the lockscreen content descriptor and Info.plist fragment.

The part you still write

Axint generates the skeleton. The actual data fetching — your API calls in getSnapshot() and timeline() — is yours to implement. The generated stubs have clear entry points:

swift
func timeline(
  for intent: SelectStockIntent,
  in context: Context,
  completion: @escaping (Timeline<StockPriceEntry>) -> Void
) {
  // Your API call here
}

Fill in the network call. The provider handles scheduling, AppKit integration, and state management. Axint gives you the structure; you write the behavior.

Lockscreen and relevance scoring

Axint also generates relevance scoring for lockscreen widgets:

swift
@available(iOS 16.1, *)
extension StockPriceWidgetEntry: TimelineEntry {
  var relevance: TimelineEntryRelevance? {
    TimelineEntryRelevance(score: Float(abs(change)))
  }
}

The scoring logic maps to your data spec. Stocks with bigger price movements surface higher on the lockscreen.

Incremental adoption

Your existing WidgetKit code doesn't need to change. axint compile generates new widgets alongside hand-written ones. Adopt one widget at a time. The generated Swift has no dependencies on Axint at runtime.