| name | pushwoosh-flutter-live-activities |
| description | Guide for integrating iOS Live Activities (Dynamic Island + Lock Screen) into Flutter apps using Pushwoosh SDK. Covers Widget Extension setup, ActivityAttributes, Flutter-to-Swift bridging, Pushwoosh API methods, and remote push updates. |
Pushwoosh Flutter SDK — iOS Live Activities Integration
This skill provides complete guidance for implementing iOS Live Activities (Dynamic Island + Lock Screen) in a Flutter project that uses the Pushwoosh Flutter SDK (pushwoosh_flutter).
Prerequisites
- iOS 16.1+ deployment target
- Pushwoosh Flutter SDK v2.3.1+
- Xcode 14.1+
PushwooshXCFramework/PushwooshLiveActivities pod (included by default since v2.3.16)
CRITICAL: Required Dart-side Pushwoosh Setup
NEVER skip this step, regardless of which approach you use.
After ALL iOS-side setup is complete, you MUST add this call on the Dart side during app initialization:
// In your main.dart or app initialization, AFTER Pushwoosh.initialize()
await Pushwoosh.getInstance.defaultSetup();
Without defaultSetup(), Pushwoosh will NOT register push-to-start tokens and remote Live Activity updates will NOT work. This call is independent from Swift/iOS code and has no build dependencies — it always works.
Add it immediately after Pushwoosh.initialize(), before any other Live Activity code.
Architecture Overview
Flutter (Dart) iOS (Swift) System
───────────── ─────────── ──────
DynamicIslandManager ──────► LiveActivityManager ──────► ActivityKit
(MethodChannel "PW") (start/update/stop) (Dynamic Island)
Pushwoosh Dart API ──────► PushwooshPlugin.m ──────► Pushwoosh SDK
(defaultSetup/Start) (MethodChannel (Token registration
startWithToken) "pushwoosh") + Remote updates)
Two approaches:
- Custom Live Activity — full control, custom UI via Widget Extension + MethodChannel bridge
- Default Pushwoosh Live Activity — simpler, uses
defaultSetup() / defaultStart(), managed by Pushwoosh
Approach 1: Custom Live Activity (Full Control)
Step 1: Enable Live Activities in Info.plist
In the Runner iOS app's Info.plist:
<key>NSSupportsLiveActivities</key>
<true/>
Step 2: Create Widget Extension Target
- In Xcode: File → New → Target → Widget Extension
- Name it (e.g.,
MyAppWidgetExtension)
- Set deployment target to iOS 16.1
- Uncheck "Include Configuration App Intent" (not needed for Live Activities)
Step 3: Define ActivityAttributes (Swift)
Create a shared Swift file accessible to both the main app and widget extension. Add to both targets.
import ActivityKit
import Foundation
struct MyActivityAttributes: ActivityAttributes {
var title: String
public struct ContentState: Codable, Hashable {
var value: Int
var statusMessage: String
}
}
Step 4: Implement LiveActivityManager (Swift)
Create LiveActivityManager.swift in the Runner target:
import ActivityKit
import Flutter
import Foundation
@available(iOS 16.1, *)
class LiveActivityManager {
private var currentActivity: Activity<MyActivityAttributes>? = nil
func startLiveActivity(data: [String: Any]?, result: FlutterResult) {
guard let info = data else {
result(FlutterError(code: "INVALID_DATA", message: "Data is nil", details: nil))
return
}
let attributes = MyActivityAttributes(
title: info["title"] as? String ?? ""
)
let state = MyActivityAttributes.ContentState(
value: info["value"] as? Int ?? 0,
statusMessage: info["statusMessage"] as? String ?? ""
)
do {
currentActivity = try Activity<MyActivityAttributes>.request(
attributes: attributes,
contentState: state,
pushType: nil
)
} catch {
result(FlutterError(code: "START_FAILED", message: error.localizedDescription, details: nil))
}
}
func updateLiveActivity(data: [String: Any]?, result: FlutterResult) {
guard let info = data else {
result(FlutterError(code: "INVALID_DATA", message: "Data is nil", details: nil))
return
}
let updatedState = MyActivityAttributes.ContentState(
value: info["value"] as? Int ?? 0,
statusMessage: info["statusMessage"] as? String ?? ""
)
Task {
await currentActivity?.update(using: updatedState)
}
}
func stopLiveActivity(result: FlutterResult) {
Task {
await currentActivity?.end(using: nil, dismissalPolicy: .immediate)
}
}
}
Step 5: Wire up AppDelegate (Swift)
In AppDelegate.swift, set up the MethodChannel bridge:
import UIKit
import Flutter
@main
@objc class AppDelegate: FlutterAppDelegate {
private let liveActivityManager = LiveActivityManager()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(
name: "PW",
binaryMessenger: controller.binaryMessenger
)
channel.setMethodCallHandler { [weak self] (call, result) in
switch call.method {
case "startLiveActivity":
self?.liveActivityManager.startLiveActivity(
data: call.arguments as? [String: Any], result: result)
case "updateLiveActivity":
self?.liveActivityManager.updateLiveActivity(
data: call.arguments as? [String: Any], result: result)
case "stopLiveActivity":
self?.liveActivityManager.stopLiveActivity(result: result)
default:
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Step 6: Create Widget UI (SwiftUI)
In the Widget Extension target, create the Live Activity widget:
import SwiftUI
import WidgetKit
import ActivityKit
struct MyLiveActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: MyActivityAttributes.self) { context in
HStack {
VStack(alignment: .leading) {
Text(context.attributes.title)
.font(.headline)
.foregroundColor(.white)
Text(context.state.statusMessage)
.font(.subheadline)
.foregroundColor(.white.opacity(0.7))
}
Spacer()
Text("\(context.state.value)")
.font(.system(size: 32, weight: .bold))
.foregroundColor(.yellow)
}
.padding()
.activityBackgroundTint(.black.opacity(0.5))
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
VStack {
Text(context.attributes.title)
.font(.headline)
Text(context.state.statusMessage)
.font(.subheadline)
Text("\(context.state.value)")
.font(.title)
.bold()
.foregroundColor(.yellow)
}
}
} compactLeading: {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
} compactTrailing: {
Text("\(context.state.value)")
.foregroundColor(.yellow)
} minimal: {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
}
}
Step 7: Flutter Dart Side — DynamicIslandManager
import 'dart:developer';
import 'package:flutter/services.dart';
class DynamicIslandManager {
final String channelKey;
late final MethodChannel _methodChannel;
DynamicIslandManager({required this.channelKey}) {
_methodChannel = MethodChannel(channelKey);
}
Future<void> startLiveActivity({required Map<String, dynamic> data}) async {
try {
await _methodChannel.invokeMethod('startLiveActivity', data);
} catch (e, st) {
log('startLiveActivity error: $e', stackTrace: st);
}
}
Future<void> updateLiveActivity({required Map<String, dynamic> data}) async {
try {
await _methodChannel.invokeMethod('updateLiveActivity', data);
} catch (e, st) {
log('updateLiveActivity error: $e', stackTrace: st);
}
}
Future<void> stopLiveActivity() async {
try {
await _methodChannel.invokeMethod('stopLiveActivity');
} catch (e, st) {
log('stopLiveActivity error: $e', stackTrace: st);
}
}
}
Usage:
final diManager = DynamicIslandManager(channelKey: 'PW');
// Start
await diManager.startLiveActivity(data: {
'title': 'My Activity',
'value': 0,
'statusMessage': 'Starting...',
});
// Update
await diManager.updateLiveActivity(data: {
'value': 42,
'statusMessage': 'In progress...',
});
// Stop
await diManager.stopLiveActivity();
Approach 2: Custom Pushwoosh Live Activity with setup()
Uses your own ActivityAttributes conforming to PushwooshLiveActivityAttributes protocol. Pushwoosh automatically handles pushToStart and pushToUpdate token management.
Define Custom Attributes (Swift)
Your attributes must conform to PushwooshLiveActivityAttributes, and ContentState must conform to PushwooshLiveActivityContentState:
import ActivityKit
import PushwooshLiveActivities
struct MyCustomAttributes: PushwooshLiveActivityAttributes {
public struct ContentState: PushwooshLiveActivityContentState {
var value: Int
var statusMessage: String
var pushwoosh: PushwooshLiveActivityContentStateData?
}
var title: String
var pushwoosh: PushwooshLiveActivityAttributeData
}
Important: Both pushwoosh properties are required by the protocol. They are used internally for token management.
Register in AppDelegate (Swift)
Call setup() early in app lifecycle — it automatically listens for token updates:
import PushwooshFramework
import PushwooshLiveActivities
if #available(iOS 16.1, *) {
Pushwoosh.LiveActivities.setup(MyCustomAttributes.self)
}
Start / Update / Stop from Flutter
Use startLiveActivityWithToken() to register your custom activity token with Pushwoosh, then manage the activity via MethodChannel bridge (same as Approach 1).
Approach 3: Default Pushwoosh Live Activity
Uses built-in Pushwoosh DefaultLiveActivityAttributes. Simplest approach, but no custom UI.
Setup
import 'package:pushwoosh_flutter/pushwoosh_flutter.dart';
// During app initialization (after Pushwoosh.initialize)
await Pushwoosh.getInstance.defaultSetup();
defaultSetup() registers the device's push-to-start token with Pushwoosh servers. Call it once during app init.
Start Locally
await Pushwoosh.getInstance.defaultStart(
"my_activity_id", // unique activity ID
{"driverName": "John"}, // static attributes (cannot change)
{"status": "On the way"}, // dynamic content state (can update)
);
Start with Custom Token
await Pushwoosh.getInstance.startLiveActivityWithToken(
token, // Activity push token string
activityId, // Activity ID
);
Stop
await Pushwoosh.getInstance.stopLiveActivity();
Remote Update via Pushwoosh API
Send POST to https://go.pushwoosh.com/json/1.3/createMessage:
{
"request": {
"application": "XXXXX-XXXXX",
"auth": "YOUR_AUTH_API_TOKEN",
"notifications": [
{
"content": "Update",
"title": "Title",
"live_activity": {
"event": "update",
"content-state": {
"data": {"status": "Delivered"}
},
"attributes-type": "DefaultLiveActivityAttributes",
"attributes": {
"data": {"driverName": "John"}
}
},
"devices": ["DEVICE_HWID"],
"live_activity_id": "my_activity_id"
}
]
}
}
Events: "start", "update", "end"
Pushwoosh Flutter SDK API Reference (Live Activities)
| Method | Where | Description |
|---|
Pushwoosh.LiveActivities.setup(MyAttributes.self) | iOS (Swift) | Register custom ActivityAttributes with automatic token management |
defaultSetup() | Dart / iOS | Register default push-to-start token with Pushwoosh |
defaultStart(activityId, attributes, content) | Dart / iOS | Start default Live Activity |
startLiveActivityWithToken(token, activityId) | Dart / iOS | Register custom activity token with Pushwoosh |
stopLiveActivity() | Dart / iOS | Stop current Live Activity |
iOS Version Check
The SDK checks iOS version before calling Live Activities APIs:
if ([PushwooshPlugin isSystemVersionGreaterOrEqualTo:@"16.1"]) {
[PushwooshLiveActivitiesImplementationSetup defaultSetup];
}
Always guard Live Activity code with @available(iOS 16.1, *).
Common Issues
Activity not appearing
- Verify
NSSupportsLiveActivities = YES in Info.plist
- Check deployment target is iOS 16.1+
- Ensure
ActivityAttributes struct is in shared group (both app and extension targets)
- Test on physical device (Simulator has limited support)
MethodChannel not working
- Ensure channel name matches on both sides (
"PW")
setMethodCallHandler must be called in didFinishLaunchingWithOptions
- Check Flutter controller is not nil
Push updates not arriving
- Call
defaultSetup() after Pushwoosh.initialize()
- Verify push certificate supports Live Activities
- Check
live_activity_id matches between start and update calls
Widget Extension build errors
- Add
ActivityAttributes file to both targets (Runner + Widget Extension)
- Import
ActivityKit in all files that use Activity types
- Pod dependency
PushwooshXCFramework/PushwooshLiveActivities must be in Podfile
Podspec Dependency
The Pushwoosh Flutter SDK podspec (v2.3.16) includes Live Activities by default:
s.dependency 'PushwooshXCFramework', '7.0.22'
s.dependency 'PushwooshXCFramework/PushwooshLiveActivities'
No additional pod configuration needed.
CRITICAL: Add All Created Files to the Xcode Project
Every Swift file you create MUST be registered in the Xcode project (project.pbxproj). Simply creating a file on disk is NOT enough — Xcode will not compile it unless it is added to the project and target.
After creating any .swift file, you MUST:
- Add a
PBXFileReference entry for the file
- Add a
PBXBuildFile entry linking the file reference to the target
- Add the file reference to the appropriate
PBXGroup (so it appears in the Xcode file navigator)
- Add the build file to the
PBXSourcesBuildPhase of the correct target (Runner or Widget Extension)
This applies to ALL files: ActivityAttributes.swift, LiveActivityManager.swift, Widget Extension Swift files, etc.
If you skip this step, the project will compile but the new files will be completely ignored, causing runtime failures with no build errors.
For Widget Extension files, make sure they are added to the Widget Extension target, not the Runner target. For shared files (like ActivityAttributes.swift), add them to both targets.
Files to Create/Modify Checklist
Create (and add to Xcode project!):
Modify: