Live Activities
Getting Started
Section titled “Getting Started”| GitHub | |
| Downloads |
- MyApp/
1<?xml version="1.0" encoding="utf-8"?>2<manifest xmlns:android="http://schemas.android.com/apk/res/android">3 <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">4 </application>5 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>6 <uses-permission android:name="android.permission.BATTERY_STATS" />7 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />8 <uses-permission android:name="android.permission.INTERNET" />9 <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />10</manifest>A Live Activity is the persistent, updating status surface both phone platforms grew independently — a delivery on its way, a match score, a workout, a long upload. Shiny puts one typed API in front of both: iOS/iPadOS Live Activities (ActivityKit — Lock Screen and Dynamic Island) and Android 16 Live Updates (a promoted ongoing notification that also renders as a status bar chip and on the always-on display).
The shared contract is a state, not a UI tree. The two platforms render very differently — iOS runs
arbitrary SwiftUI from a widget extension, Android draws a notification — so what crosses the boundary is a
typed LiveActivityContent and nothing else.
Platform support
Section titled “Platform support”| Platform | What you get |
|---|---|
| iOS / iPadOS 16.2+ | A real Live Activity through ActivityKit, rendered by a SwiftUI widget extension the package builds into your app (ShinyLiveActivityWidget) |
| Android 16 (API 36)+ | Notification.ProgressStyle with requestPromotedOngoing and setShortCriticalText — status bar chip and AOD |
| Android 8–15 | An ordinary ongoing notification with a determinate progress bar, no chip |
| macOS, Mac Catalyst, tvOS, Windows, Linux, Blazor | NoOpLiveActivityManager — IsSupported is false and every call is a safe no-op |
Because unsupported platforms no-op rather than throw, shared view models need no #if. Branch on
IsSupported only when the UI should hide the feature entirely.
Packages
Section titled “Packages”| Package | Role |
|---|---|
Shiny.Mobile.LiveActivities |
The cross-platform API. This is the one you reference. |
Shiny.iOS.LiveActivities.Binding |
The ActivityKit Swift shim, pulled in automatically on iOS. Never reference it directly. |
ActivityKit is Swift-only — it has no Objective-C interface, so there is nothing for the .NET iOS SDK to
bind directly. The binding package wraps it in an @objc bridge built from an Xcode project, which is also
why the repo’s CI runs on macOS.
The namespace is Shiny.LiveActivities; the Mobile segment is a package-scope qualifier, not part of the
API surface.
Registration
Section titled “Registration”builder.Services.AddLiveActivities();
// or, if a server pushes updates — the delegate is the only way to learn the tokensbuilder.Services.AddLiveActivities<MyLiveActivityDelegate>();Both overloads take an optional Action<LiveActivityOptions>. It is Android-only — iOS has no
notification channel and nothing app-settable to configure:
builder.Services.AddLiveActivities(o =>{ o.ChannelName = Strings.LiveActivityChannelName; o.ChannelDescription = Strings.LiveActivityChannelBlurb;});| Option | Default | What it does |
|---|---|---|
ChannelName |
"Live Activities" |
The channel name shown in Android’s per-app notification settings |
ChannelDescription |
"Ongoing updates such as deliveries, timers and scores" |
The description under it; null leaves it blank |
iOS: turn on the widget extension
Section titled “iOS: turn on the widget extension”iOS draws a Live Activity from a SwiftUI widget extension in your app bundle. Shiny builds one for you — add
this to your app’s .csproj and there is no Xcode project, no Swift and no Info.plist edit:
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'"> <ShinyLiveActivityWidget>true</ShinyLiveActivityWidget></PropertyGroup>Skip it and Start still succeeds on iOS, but nothing appears. Signing, provisioning profiles and your own
layout are covered on the widget extension page. Android needs nothing.
Start, update, end
Section titled “Start, update, end”public class OrderTracker(ILiveActivityManager activities){ string? activityId;
public async Task Begin(string orderNumber) { if (!activities.IsSupported) return;
if (await activities.RequestAccess() != AccessState.Available) return;
var activity = await activities.Start(new LiveActivityRequest { // static for the activity's whole life — on iOS this becomes ShinyActivityAttributes Attributes = new Dictionary<string, string> { ["orderNumber"] = orderNumber }, Kind = "delivery", Content = new LiveActivityContent { Title = "Order confirmed", Body = "Preparing your order", ShortStatus = "0%", Progress = LiveActivityProgress.FromValue(0.0) } }); this.activityId = activity.Id; }
public Task Advance(double percent, string body) => activities.Update( this.activityId!, new LiveActivityContent { Title = "Out for delivery", Body = body, ShortStatus = $"{percent:P0}", Progress = LiveActivityProgress.FromValue(percent) } );
public Task Finish() => activities.End( this.activityId!, new LiveActivityContent { Title = "Delivered", Progress = LiveActivityProgress.FromValue(1.0) }, dismissAt: DateTimeOffset.UtcNow.AddMinutes(2) );}GetAll() lists what is running, newest first. EndAll() clears everything — call it on logout. Pass a
LiveActivityAlert to Update to surface a banner and a tap on a paired watch instead of refreshing
silently.
Content
Section titled “Content”| Property | Purpose |
|---|---|
Title |
The headline — order status, team names, “Arriving in 5 min” |
Body |
Supporting detail under the title |
ShortStatus |
A handful of characters for the tightest surfaces: the Dynamic Island compact view and the Android status bar chip |
Progress |
Optional progress indicator — see below |
StaleDate |
When the content should be treated as out of date; the system flips the activity to LiveActivityState.Stale so your widget can render an “out of date” view. It does not end the activity |
RelevanceScore |
Ranks this activity against your app’s others for the Dynamic Island (iOS) |
Data |
Free-form string/string values your own widget reads |
What you can drive from C#, and what you can’t
Section titled “What you can drive from C#, and what you can’t”Worth understanding before you design against this, because it shapes what fits.
The layout is never C#
Section titled “The layout is never C#”WidgetKit requires the Lock Screen and Dynamic Island views to be SwiftUI, in a widget extension signed
with your own bundle id. No NuGet package can change that. C# only ever sends state; the drawing is
SwiftUI. Set ShinyLiveActivityWidget and the package builds a stock widget extension into your
app, so most apps never write Swift or open Xcode; a custom look is a few Swift
files the same build compiles for you.
Android has no equivalent constraint; the library posts the notification itself.
There is exactly one ActivityAttributes type
Section titled “There is exactly one ActivityAttributes type”ActivityKit needs a concrete Codable type at compile time, which would normally force every app to
hand-write its own Swift — and makes a general-purpose .NET API impossible. Shiny pins a single
ShinyActivityAttributes instead, with the fields that matter to the system strongly typed and everything
app-specific in string dictionaries:
| Strongly typed | Free-form |
|---|---|
title, body, shortStatus, progress, progressStart, progressEnd, indeterminate |
data (changes over the activity’s life), values (static, from LiveActivityRequest.Attributes) |
That buys the cross-platform API, and it costs three things:
- Custom fields are strings. A number, date, nested object or array gets flattened on the C# side and parsed in your Swift. Only the progress family is genuinely typed.
- One attributes type for the whole app. Apple lets you declare several distinct
ActivityAttributesstructs; here you get one, withKindas a discriminator your widget branches on (switch context.attributes.kind). A push-to-start payload must nameShinyActivityAttributesas itsattributes-type. - The 4KB content-state cap is easier to hit when everything is a string.
Not reachable through this package
Section titled “Not reachable through this package”- Interactive activities. A
ButtonorTogglebacked by anAppIntentlives in the extension, in Swift, andILiveActivityDelegatehas no action callback — onlyOnStarted,OnStateChangedand the two token events. Bridging one back to .NET is on you (an app group, a URL scheme). - Alert sound.
LiveActivityAlertcarries a title and body; Apple’sAlertConfigurationalso takes a sound.
So what fits?
Section titled “So what fits?”Anything whose changing state is title, body, short status, progress, stale date, relevance score and a string bag — which is most real activities: delivery tracking, rideshare ETA, sports scores, timers, workouts, transfer progress.
If you genuinely need your own typed schema, the escape hatch is forking ShinyActivityAttributes.swift
(in both native/ShinyLiveActivities/ and templates/WidgetExtension/, which the widget build compiles)
along with LiveActivityContentSchema. That puts you off the contract shared with Shiny.Extensions.Push, where
drift fails silently rather than throwing — so weigh it.
Prefer a time range over a fraction
Section titled “Prefer a time range over a fraction”// self-advancing: the system animates it with no further updates from youProgress = LiveActivityProgress.FromRange(startedAt, expectedFinish)Every push update costs budget, and on iOS a suspended app sends none at all — a fraction-based bar simply
freezes until the app wakes. A range keeps advancing with no app involvement, and each real update
re-anchors it. Use FromValue when the fraction is genuinely known and not time-shaped, and
Indeterminate = true when it is unknown.
Permissions
Section titled “Permissions”GetCurrentAccess() and RequestAccess() report the iOS per-app Live Activities switch, or notification
permission on Android. iOS has no prompt for Live Activities, so RequestAccess there simply reports the
current setting; Android 13+ asks for POST_NOTIFICATIONS.
Already done for you: HTTP transfer progress
Section titled “Already done for you: HTTP transfer progress”Do not hand-roll a Live Activity for Shiny.Net.Http uploads and downloads.
AddTransferProgress() already drives one on iOS and the foreground-service
notification on Android, from a single manager, with no code in your transfer delegate.
- The iOS widget extension — built for you with
ShinyLiveActivityWidget; signing, profiles and custom layouts - Push tokens and server updates


