Zum Inhalt springen

Live Activity für iOS in einem Flutter-Projekt

Wir werden eine Live Activity mit einer Stoppuhr-Implementierung für ein Flutter-Projekt erstellen. Das Projekt wird sowohl eine Implementierung auf der Flutter-Seite als auch eine native Implementierung im Xcode-Projekt umfassen.

Implementierung auf der Flutter-Seite

Anchor link to

Wir werden eine Klasse erstellen, die drei Methoden implementiert:

  • startLiveActivity()
  • updateLiveActivity()
  • stopLiveActivity().
class DynamicIslandManager {
final String channelKey;
late final MethodChannel _methodChannel;
DynamicIslandManager({required this.channelKey}) {
_methodChannel = MethodChannel(channelKey);
}
Future<void> startLiveActivity({required Map<String, dynamic> jsonData}) async {
try {
await _methodChannel.invokeListMethod('startLiveActivity', jsonData);
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
Future<void> updateLiveActivity(
{required Map<String, dynamic> jsonData}) async {
try {
await _methodChannel.invokeListMethod('updateLiveActivity', jsonData);
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
Future<void> stopLiveActivity() async {
try {
await _methodChannel.invokeListMethod('stopLiveActivity');
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
}

Wir werden eine Stoppuhr-App erstellen. Um die Timer-Daten der Stoppuhr an native Methoden zu übergeben, müssen wir ein Datenmodell erstellen und es im JSON-ähnlichen Map-Format an den Method-Channel-Aufruf senden.

class DynamicIslandStopwatchDataModel {
final int elapsedSeconds;
DynamicIslandStopwatchDataModel({
required this.elapsedSeconds,
});
Map<String, dynamic> toMap() {
return <String, dynamic>{
'elapsedSeconds': elapsedSeconds,
};
}
}

Jede Methode wird den notwendigen Code enthalten, um die entsprechenden nativen Methoden aufzurufen.

Dies war die anfängliche Einrichtung, die für die Ausführung auf der Flutter-Seite erforderlich war. Anschließend wurde eine einfache Benutzeroberfläche für die Stoppuhr-App mit den notwendigen Methodenaufrufen implementiert.

class _StopWatchScreenState extends State<StopWatchScreen> {
int seconds = 0;
bool isRunning = false;
Timer? timer;
/// channel key is used to send data from flutter to swift side over
/// a unique bridge (link between flutter & swift)
final DynamicIslandManager diManager = DynamicIslandManager(channelKey: 'PW');
void startTimer() {
setState(() {
isRunning = true;
});
// invoking startLiveActivity Method
diManager.startLiveActivity(
jsonData: DynamicIslandStopwatchDataModel(elapsedSeconds: 0).toMap(),
);
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
seconds++;
});
// invoking the updateLiveActivity Method
diManager.updateLiveActivity(
jsonData: DynamicIslandStopwatchDataModel(
elapsedSeconds: seconds,
).toMap(),
);
});
}
void stopTimer() {
timer?.cancel();
setState(() {
seconds = 0;
isRunning = false;
});
// invoking the stopLiveActivity Method
diManager.stopLiveActivity();
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Stopwatch App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Stopwatch: $seconds seconds',
style: const TextStyle(fontSize: 24),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: isRunning ? null : startTimer,
child: const Text('Start'),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: isRunning ? stopTimer : null,
child: const Text('Stop'),
),
],
),
],
),
),
);
}
}

Implementierung auf der nativen Seite

Anchor link to

Konfiguration des Xcode-Projekts

Anchor link to
  1. Öffnen Sie Ihr Projekt in Xcode und stellen Sie im Tab ‘Allgemein’ die minimale Bereitstellung auf iOS 16.1 ein.
Xcode-Tab 'Allgemein', der die minimale Bereitstellung auf iOS 16.1 zeigt
  1. Fügen Sie in Info.plist einen neuen Schlüssel NSSupportsLiveActivities hinzu und setzen Sie dessen Wert auf YES (Boolean).

Info.plist

Anchor link to
<key>NSSupportsLiveActivities</key>
<true/>
  1. Wählen Sie in der Aktionsleiste File > New > Target, suchen Sie nach WidgetExtension und erstellen Sie ein neues Widget.

Implementierung auf der Code-Seite

Anchor link to

Erstellen wir eine Klasse namens LiveActivityManager, die die Live Activities für unsere Dynamic Island verwaltet. Diese Klasse wird drei Methoden enthalten: startLiveActivity(), updateLiveActivity() und stopLiveActivity().

Swift

import ActivityKit
import Flutter
import Foundation
@available(iOS 16.1, *)
class LiveActivityManager {
private var stopwatchActivity: Activity<StopwatchWidgetAttributes>? = nil
func startLiveActivity(data: [String: Any]?, result: FlutterResult) {
let attributes = StopwatchWidgetAttributes()
if let info = data {
let state = StopwatchWidgetAttributes.ContentState(
elapsedTime: info["elapsedSeconds"] as? Int ?? 0
)
stopwatchActivity = try? Activity<StopwatchWidgetAttributes>.request(
attributes: attributes, contentState: state, pushType: nil)
} else {
result(FlutterError(code: "418", message: "Live activity didn't invoked", details: nil))
}
}
func updateLiveActivity(data: [String: Any]?, result: FlutterResult) {
if let info = data {
let updatedState = StopwatchWidgetAttributes.ContentState(
elapsedTime: info["elapsedSeconds"] as? Int ?? 0
)
Task {
await stopwatchActivity?.update(using: updatedState)
}
} else {
result(FlutterError(code: "418", message: "Live activity didn't updated", details: nil))
}
}
func stopLiveActivity(result: FlutterResult) {
do {
Task {
await stopwatchActivity?.end(using: nil, dismissalPolicy: .immediate)
}
} catch {
result(FlutterError(code: "418", message: error.localizedDescription, details: nil))
}
}
}

Jede Methode hat ihre spezifische Funktionalität. startLiveActivity() ist (wie der Name schon sagt) für das Starten der Live Activity verantwortlich, was die Funktionalität der Dynamic Island auslöst. In ähnlicher Weise kümmern sich stopLiveActivity() und updateLiveActivity() um das Beenden der Live Activity und das Aktualisieren der auf der Dynamic Island angezeigten Daten.

Öffnen Sie als Nächstes StopWatchDIWidgetLiveActivity.swift und ändern Sie die StopwatchDIWidgetAttributes-Struktur (wie im Snippet gezeigt). Diese Attributstruktur dient als Datenmodell, das die auf der Benutzeroberfläche anzuzeigenden Informationen (direkt von Flutter) enthält und bei Bedarf auch die Benutzeroberfläche ändern kann.

Swift

struct StopwatchWidgetAttributes: ActivityAttributes {
public typealias stopwatchStatus = ContentState
public struct ContentState: Codable, Hashable {
var elapsedTime: Int
}
}

Jetzt fehlt nur noch die Benutzeroberfläche für die Dynamic Island! (Juhu, wir haben es so weit geschafft!) Die gesamte Benutzeroberfläche für die Dynamic Island muss mit SwiftUI erstellt werden. Für diesen Artikel wurde eine einfache Benutzeroberfläche entworfen (Sie können sie aber nach Belieben anpassen).

Dieser UI-Code sollte innerhalb der StopwatchWidgetLiveActivity-Struktur geschrieben werden. Entfernen Sie den vorhandenen Code aus der Struktur und folgen Sie dem unten stehenden Code:

SwiftUI

struct StopwatchWidgetLiveActivity: Widget {
func getTimeString(_ seconds: Int) -> String {
let hours = seconds / 3600
let minutes = (seconds % 3600) / 60
let seconds = (seconds % 3600) % 60
return hours == 0 ?
String(format: "%02d:%02d", minutes, seconds)
: String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
var body: some WidgetConfiguration {
ActivityConfiguration(for: StopwatchWidgetAttributes.self) { context in
HStack {
Text("Time ellapsed")
.font(.system(size: 20, weight: .semibold))
.foregroundColor(.white)
Spacer()
Image(systemName: "timer")
.foregroundColor(.white)
Spacer().frame(width: 10)
Text(getTimeString(context.state.elapsedTime))
.font(.system(size: 24, weight: .semibold))
.foregroundColor(.yellow)
}
.padding(.horizontal)
.activityBackgroundTint(Color.black.opacity(0.5))
}
dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
VStack(alignment: .center) {
Text("Pushwoosh Timer")
Spacer().frame(height: 24)
HStack {
Text("Time ellapsed")
.font(.system(size: 20, weight: .semibold))
.foregroundColor(.white)
Spacer()
Image(systemName: "timer")
Spacer().frame(width: 10)
Text(getTimeString(context.state.elapsedTime))
.font(.system(size: 24, weight: .semibold))
.foregroundColor(.yellow)
}.padding(.horizontal)
}
}
} compactLeading: {
Image(systemName: "timer").padding(.leading, 4)
} compactTrailing: {
Text(getTimeString(context.state.elapsedTime)).foregroundColor(.yellow)
.padding(.trailing, 4)
} minimal: {
Image(systemName: "timer")
.foregroundColor(.yellow)
.padding(.all, 4)
}
.widgetURL(URL(string: "http://www.pushwoosh.com"))
.keylineTint(Color.red)
}
}
}

Vergessen Sie nicht, auch den AppDelegate-Code zu ändern.

AppDelegate.swift

import UIKit
import Flutter
@main
@objc class AppDelegate: FlutterAppDelegate {
private let liveActivityManager: LiveActivityManager = LiveActivityManager()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let diChannel = FlutterMethodChannel(name: "PW", binaryMessenger: controller.binaryMessenger)
diChannel.setMethodCallHandler({ [weak self] (
call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "startLiveActivity":
self?.liveActivityManager.startLiveActivity(
data: call.arguments as? Dictionary<String,Any>,
result: result)
break
case "updateLiveActivity":
self?.liveActivityManager.updateLiveActivity(
data: call.arguments as? Dictionary<String,Any>,
result: result)
break
case "stopLiveActivity":
self?.liveActivityManager.stopLiveActivity(result: result)
break
default:
result(FlutterMethodNotImplemented)
}
})
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

Und das war’s! Das deckt alles ab, was zur Implementierung der Dynamic Island und zur Verknüpfung mit dem Flutter-Code erforderlich ist!

Implementierung des Pushwoosh-Codes

Anchor link to

Das Pushwoosh-Flutter-Plugin bietet zwei Methoden zur Verwaltung von Live Activity im Projekt.

/// Default setup Live Activity
Future<void> defaultSetup() async {
await _channel.invokeMethod("defaultSetup");
}
/// Default start Live Activity
/// [activityId] activity ID
/// [attributes] attributes
/// [content] content
Future<void> defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content) async {
await _channel.invokeMethod("defaultStart", {"activityId": activityId, "attributes": attributes, "content": content});
}

Die Methode defaultSetup() ermöglicht es Ihnen, die Struktur der Live Activity und der Tokens auf der Pushwoosh-Seite zu verwalten.

Rufen Sie diese Methode während der Initialisierung der Anwendung auf.

Pushwoosh.initialize({"app_id": "XXXXX-XXXXX", "sender_id": "XXXXXXXXXXXX"});
/**
* Call this method `defaultSetup()`
*/
Pushwoosh.getInstance.defaultSetup();

Während der Initialisierung der App sendet Pushwoosh Ihr Push-to-Start-Token an den Server, was es Ihnen später ermöglicht, die Live Activity über einen API-Aufruf zu starten. Um die Live Activity zu starten, müssen Sie eine API-Anfrage stellen. Unten finden Sie ein Beispiel für die Anfrage:

{
"request": {
"application": "XXXXX-XXXXX",
"auth": "YOUR_AUTH_API_TOKEN",
"notifications": [
{
"content": "Message",
"title":"Title",
"live_activity": {
"event": "start", // `start` event
"content-state": {
"data": { // You need to pass the parameters in a dictionary with the key data.
"emoji": "dynamic data"
}
},
"attributes-type": "DefaultLiveActivityAttributes",
"attributes": {
"data": {
"name": "static data"
}
}
},
"devices": [ "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" // HWID
],
"live_activity_id": "your_activity_id"
}
]
}
}

Ausführlichere Dokumentation zur API

Wenn Sie eine einfache Live Activity, zum Beispiel aus Ihrer App heraus, starten möchten, können Sie die folgende Methode in Flutter verwenden: defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)

Diese Methode kann bei jedem Ereignis aufgerufen werden, das den Start der Live Activity auslöst.

// Function to start Live Activity
void startLiveActivity() {
// Create your activity ID
String activityId = "stopwatch_activity";
// Define the attributes you want to send to the Live Activity
Map<String, dynamic> attributes = {
'title': 'Stopwatch Activity',
'description': 'This is a live activity for a stopwatch.'
};
// Define the content state to update on the Dynamic Island
Map<String, dynamic> content = {
'elapsedSeconds': 0
};
// Call Pushwoosh's defaultStart method to trigger the Live Activity
Pushwoosh.getInstance().defaultStart(activityId, attributes, content);
}