The iOS Widget Extension
iOS renders a Live Activity from a SwiftUI widget extension inside your app bundle. WidgetKit requires
it — there is no way to drive the layout from C#. Shiny.Mobile.LiveActivities builds that extension for
you as part of your app’s normal dotnet build: no Xcode project, no Swift to copy, nothing to wire up.
Android needs none of this — it draws a notification, which the library posts itself.
Turn it on
Section titled “Turn it on”In your app’s .csproj:
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'"> <ShinyLiveActivityWidget>true</ShinyLiveActivityWidget></PropertyGroup>That is the whole setup. On every iOS build the package:
- compiles the stock widget with
swiftcintoPlugIns/ShinyLiveActivity.appex, with bundle id<your-app-bundle-id>.LiveActivityand the app’s own version numbers; - picks a provisioning profile for it and signs it with the identity the app is signed with (device builds);
- adds
NSSupportsLiveActivitiesto the app’s Info.plist.
The stock widget renders whatever LiveActivityContent your app or your server sends — title, body, short
status and progress on the Lock Screen and in the Dynamic Island — so most apps never touch Swift at all.
It is opt-in so that Android-only and push-only apps never run a Swift build. It needs a Mac with Xcode, as any iOS build does; a build on Windows skips the extension with a warning.
Signing and provisioning
Section titled “Signing and provisioning”Simulator builds need nothing. On a device the extension is a second bundle, so it needs a provisioning profile for its own bundle id. Shiny looks for one the same way the .NET iOS SDK looks for the app’s:
| What | Inferred from | Override |
|---|---|---|
| Extension bundle id | the app’s bundle id + .LiveActivity |
ShinyLiveActivityBundleId |
| Signing identity | the identity the app is signed with | — (Apple requires the same team) |
| Provisioning profile | the best installed profile for the bundle id — an exact match beats a team wildcard | ShinyLiveActivityCodesignProvision (name or UUID) |
| Extra entitlements (app groups, …) | none | ShinyLiveActivityCodesignEntitlements (path to a plist) |
In practice:
-
Debug on a device — a team wildcard development profile (Xcode’s iOS Team Provisioning Profile: *) covers
<app>.LiveActivity, so it just works. -
App Store, TestFlight and Ad Hoc — a wildcard App Store profile is picked up too. With explicit profiles, create one for
<app>.LiveActivityin the Apple developer portal; once it is installed it is found automatically. Name it when several match, or in CI:<PropertyGroup Condition="'$(Configuration)' == 'Release' and $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'"><ShinyLiveActivityCodesignProvision>MyApp LiveActivity AppStore</ShinyLiveActivityCodesignProvision></PropertyGroup> -
No matching profile is a build error naming the bundle id it looked for. Shipping an unsigned extension would only fail later, at install, with a far less helpful message.
-
A different kind of profile from the app’s (Ad Hoc next to App Store, say) is a build warning — App Store Connect and device installs reject the mix.
The bundle id becomes permanent once you ship: changing it orphans the provisioning profile. Pick the override before your first release if you want a different one.
Your own layout
Section titled “Your own layout”The stock widget is ordinary SwiftUI. To replace it, add your own Swift files — with a @main
WidgetBundle — and Shiny compiles those instead:
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'"> <ShinyLiveActivityWidgetSource Include="Platforms/iOS/LiveActivity/*.swift" /></ItemGroup>import ActivityKitimport SwiftUIimport WidgetKit
@mainstruct MyLiveActivityBundle: WidgetBundle { var body: some Widget { MyLiveActivityWidget() }}
struct MyLiveActivityWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: ShinyActivityAttributes.self) { context in HStack { Text(context.state.title ?? "").font(.headline) Spacer() Text(context.state.data?["orderId"] ?? "") } .padding() } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.center) { Text(context.state.body ?? "") } } compactLeading: { Image(systemName: "shippingbox") } compactTrailing: { Text(context.state.shortStatus ?? "") } minimal: { Image(systemName: "shippingbox") } } }}Start from the stock widget (templates/WidgetExtension/ShinyLiveActivityWidget.swift in the repo) rather
than from scratch. Branch on context.attributes.kind (set from LiveActivityRequest.Kind in C#) for
different layouts per activity type, and read your own values out of context.state.data (dynamic) and
context.attributes.values (static).
Don’t declare ShinyActivityAttributes yourself. Shiny always compiles its own copy next to your files,
so the type your widget renders is exactly the one the app starts — the most common reason a hand-built
widget silently stopped updating was those two copies drifting apart.
Your sources are Swift only: there is no asset catalog or localized strings file in the extension, so use SF Symbols and text that arrives in the content.
All properties
Section titled “All properties”| Property / item | Default | Purpose |
|---|---|---|
ShinyLiveActivityWidget |
false |
Build and embed the widget extension |
ShinyLiveActivityBundleId |
$(ApplicationId).LiveActivity |
The extension’s bundle id |
ShinyLiveActivityCodesignProvision |
best installed match | Provisioning profile for the extension (name or UUID) |
ShinyLiveActivityCodesignEntitlements |
— | Extra entitlements plist for the extension |
ShinyLiveActivityMinimumOSVersion |
SupportedOSPlatformVersion, at least 16.2 |
The extension’s deployment target. An app that supports older iOS still installs there; the extension just doesn’t load |
ShinyLiveActivityDisplayName |
Live Activity |
The extension’s display name |
ShinyLiveActivitySupportsFrequentUpdates |
false |
Also adds NSSupportsLiveActivitiesFrequentUpdates — only if you update more than a handful of times an hour |
ShinyLiveActivityWidgetSource (item) |
the stock widget | Your own SwiftUI widget sources |
Keep the three definitions in step
Section titled “Keep the three definitions in step”LiveActivityContent (C#), ShinyActivityAttributes.ContentState (Swift) and the content-state a server
pushes are one contract. A field-name or type drift between them does not throw — ActivityKit decodes the
state with a stock JSONDecoder, so a mismatch just makes the update disappear and the activity stops
refreshing. The widget build removes the app-versus-widget half of that risk; the server half is still yours.
See push tokens and server updates for the wire shape, including the 2001 epoch
trap.


