iOS setup
Step 1: Initialize the iOS project
pnpm tauri ios initStep 2: Add a Widget Extension target
Open the generated Xcode project:
open src-tauri/gen/apple/*.xcodeprojIn Xcode: File → New → Target → Widget Extension. Name it (e.g. WidgetExtension), language: Swift.
In the Choose options for your new target dialog:
- Set
Product NametoWidgetExtension(or your widget name) - Select your Apple
Team(e.g. Personal Team) - Keep
Projectas your current iOS project (e.g.myapp) - Set
Embed in Applicationto your iOS app target (e.g.myapp_iOS) - For the basic plugin setup, disable:
Include Live ActivityInclude ControlInclude Configuration App Intent
Step 3: Add the TauriWidgets Swift Package
In Xcode:
- File → Add Package Dependencies...
- Click Add Local...
- Select the package folder:
- for typical app projects:
node_modules/tauri-plugin-widgets-api/swift/ - for this repository example:
swift/(repository root)
- for typical app projects:
- In Add to Target, choose your widget target (
WidgetExtension/WidgetExtensionExtension).
Important: TauriWidgets must be linked to the widget target itself. If it is linked only to the main iOS app target, import TauriWidgets fails with no such module.
Step 4: Sync the widget code via CLI (recommended)
After creating the Widget Extension target, run:
npx tauri-plugin-widgets-api init-iosThis updates the generated src-tauri/gen/apple/*/*.swift widget entry file from the plugin template using the auto-generated App Group. It also adapts the widget struct name to match Xcode-generated *Bundle.swift references, preventing cannot find 'WidgetExtension' in scope.
Manual fallback (if you prefer to edit/copy by hand):
import SwiftUI
import WidgetKit
import TauriWidgets
struct MyWidgetEntryView: View {
var entry: TauriWidgetEntry
var body: some View {
TauriWidgetView(entry: entry)
}
}
@main
struct MyWidget: Widget {
let kind = "ExampleWidget"
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
// Use the same App Group that `init-ios` generated for your project.
provider: TauriWidgetProvider(
appGroup: "group.<your-tauri-identifier>",
widgetId: "default" // must match JS setWidgetConfig widgetId
)
) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("Powered by TauriWidgets")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}Or copy from node_modules/tauri-plugin-widgets-api/templates/ios-widget/MyWidget.swift (plugin repo path: templates/ios-widget/MyWidget.swift).
Step 5: Configure App Groups
- Select the main app target (your iOS app, e.g.
myapp_iOS) → Signing & Capabilities. - Click + Capability → add App Groups.
- In the App Groups block click
+and add your group (use the value printed byinit-ios). - Repeat the same for the WidgetExtension target.
- Verify the App Group value is exactly the same in both targets.
- If + Capability is disabled, set a valid Team in Signing for that target first.
Step 6: Plugin config (tauri.conf.json)
Xcode App Groups alone are not enough. The Rust host must know the same id or plugin init fails:
{
"plugins": {
"widgets": {
"appGroup": "group.<your-tauri-identifier>",
"transport": "appGroup"
}
}
}init-iosdoes not patch this — add it yourself.- On iOS,
transportmust beappGroup(other values fail at init). - JS
setWidgetConfig(..., group, widgetId)must use that samegroupstring.
Also keep Swift TauriWidgetProvider(..., widgetId:) aligned with the JS widgetId (CLI default is "default"). If your app writes widgetId: "weather", pass widgetId: "weather" into the provider or the extension reads config:default while the host wrote config:weather.
Step 7: Run
pnpm tauri ios devWhen it fails
Widget shows "No configuration"
- Ensure the App Group identifier is identical in the main app, widget extension,
plugins.widgets.appGroup, and the JSgroupargument. - Ensure Swift
widgetIdmatches JSwidgetId(default"default"). - Call
setWidgetConfig(...)from your app before adding the widget. - Confirm plugin init succeeded (missing
plugins.widgets.appGroupaborts startup).
Widget doesn't update
- Apple limits widget refreshes to ~40–70 per day.
- Check plugin throttle:
TAURI_WIDGET_MIN_RELOAD_SECS(Debug default0, Release default900). InspectApplyOutcome.reloadfromsetWidgetConfig— throttled reloads no longer look like success-onlytrue. - For testing: in Xcode, use Debug → Simulate Timeline → After Refresh.
- For live counters, use
{ type: "timer" }instead of frequent reloads.
More symptoms: Troubleshooting index. Transport: Apple data transport.
