Live Activity de iOS en un proyecto Flutter
Crearemos una Live Activity con una implementación de cronómetro para un proyecto Flutter. El proyecto incluirá una implementación del lado de Flutter, así como una implementación nativa en el proyecto de Xcode.
Implementación del lado de Flutter
Anchor link toCrearemos una clase que implementará tres métodos:
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); } }}Estaremos construyendo una aplicación de cronómetro, y para pasar los datos del temporizador del cronómetro a los métodos nativos, necesitamos crear un modelo de datos y enviarlo a la invocación del canal de método en un formato de mapa similar a JSON.
class DynamicIslandStopwatchDataModel { final int elapsedSeconds;
DynamicIslandStopwatchDataModel({ required this.elapsedSeconds, });
Map<String, dynamic> toMap() { return <String, dynamic>{ 'elapsedSeconds': elapsedSeconds, }; }}Cada método incluirá el código necesario para llamar a los métodos nativos correspondientes.
Esta fue la configuración inicial requerida para ejecutarlo en el lado de Flutter. Después de esto, se implementó una interfaz de usuario básica de la aplicación de cronómetro con las llamadas a los métodos necesarios.
class _StopWatchScreenState extends State<StopWatchScreen> { int seconds = 0; bool isRunning = false; Timer? timer;
/// la clave del canal se utiliza para enviar datos desde el lado de flutter al de swift a través de /// un puente único (enlace entre flutter y swift) final DynamicIslandManager diManager = DynamicIslandManager(channelKey: 'PW');
void startTimer() { setState(() { isRunning = true; });
// invocando el método startLiveActivity diManager.startLiveActivity( jsonData: DynamicIslandStopwatchDataModel(elapsedSeconds: 0).toMap(), );
timer = Timer.periodic(const Duration(seconds: 1), (timer) { setState(() { seconds++; }); // invocando el método updateLiveActivity diManager.updateLiveActivity( jsonData: DynamicIslandStopwatchDataModel( elapsedSeconds: seconds, ).toMap(), ); }); }
void stopTimer() { timer?.cancel(); setState(() { seconds = 0; isRunning = false; });
// invocando el método stopLiveActivity 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'), ), ], ), ], ), ), ); }}Implementación del lado nativo
Anchor link toConfiguración del proyecto de Xcode
Anchor link to- Abre tu proyecto en Xcode y en la pestaña General establece el despliegue mínimo en iOS 16.1

- En
Info.plist, añade una nueva claveNSSupportsLiveActivitiesy establece su valor enYES(Booleano).
Info.plist
Anchor link to<key>NSSupportsLiveActivities</key><true/>- Desde la barra de acciones, selecciona Archivo > Nuevo > Destino y busca
WidgetExtensiony crea un nuevo widget.
Implementación del lado del código
Anchor link toVamos a crear una clase llamada LiveActivityManager que gestionará las actividades en vivo para nuestra Dynamic Island. Esta clase contendrá tres métodos: startLiveActivity(), updateLiveActivity() y stopLiveActivity().
Swift
import ActivityKitimport Flutterimport 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)) } }}Cada método tiene su funcionalidad específica. startLiveActivity() (como su nombre indica) es responsable de iniciar la Live Activity, lo que activa la funcionalidad de la Dynamic Island. De manera similar, stopLiveActivity() y updateLiveActivity() se encargan de detener la Live Activity y actualizar los datos que se muestran en la Dynamic Island.
A continuación, abre StopWatchDIWidgetLiveActivity.swift y modifica la estructura StopwatchDIWidgetAttributes (como se muestra en el fragmento). Esta estructura de atributos sirve como el modelo de datos que contiene la información que se mostrará en la interfaz de usuario (directamente desde Flutter) y también puede modificar la interfaz de usuario según sea necesario.
Swift
struct StopwatchWidgetAttributes: ActivityAttributes { public typealias stopwatchStatus = ContentState
public struct ContentState: Codable, Hashable { var elapsedTime: Int }}¡Ahora, lo único que queda es la interfaz de usuario para la Dynamic Island! (¡Woohoo, hemos llegado hasta aquí!) Toda la interfaz de usuario para la Dynamic Island debe crearse con SwiftUI. Para este artículo, se ha diseñado una interfaz de usuario sencilla (pero siéntete libre de personalizarla según sea necesario).
Este código de la interfaz de usuario debe escribirse dentro de la estructura StopwatchWidgetLiveActivity. Elimina el código existente de la estructura y puedes seguir el código a continuación:
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) } }}No olvides modificar también el código de AppDelegate.
AppDelegate.swift
import UIKitimport 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) }}¡Y eso es todo! ¡Eso cubre todo lo necesario para implementar la Dynamic Island y vincularla al código de Flutter!
Implementación del código de Pushwoosh
Anchor link toEl plugin de Flutter de Pushwoosh proporciona dos métodos para gestionar la Live Activity en el proyecto.
/// Configuración por defecto de Live Activity Future<void> defaultSetup() async { await _channel.invokeMethod("defaultSetup"); }
/// Inicio por defecto de Live Activity /// [activityId] ID de la actividad /// [attributes] atributos /// [content] contenido Future<void> defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content) async { await _channel.invokeMethod("defaultStart", {"activityId": activityId, "attributes": attributes, "content": content}); }El método defaultSetup() te permite gestionar la estructura de la Live Activity y los tokens del lado de Pushwoosh.
Llama a este método durante la inicialización de la aplicación.
Pushwoosh.initialize({"app_id": "XXXXX-XXXXX", "sender_id": "XXXXXXXXXXXX"});/*** Llama a este método `defaultSetup()`*/ Pushwoosh.getInstance.defaultSetup();Durante la inicialización de la aplicación, Pushwoosh enviará tu token de push-to-start al servidor, lo que te permitirá más tarde iniciar la Live Activity a través de una llamada a la API. Para iniciar la Live Activity, necesitas hacer una solicitud a la API. A continuación se muestra un ejemplo de la solicitud:
{ "request": { "application": "XXXXX-XXXXX", "auth": "YOUR_AUTH_API_TOKEN", "notifications": [ { "content": "Message", "title":"Title", "live_activity": { "event": "start", // evento `start` "content-state": { "data": { // Necesitas pasar los parámetros en un diccionario con la clave 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" } ] }}Documentación más detallada sobre la API
Si quieres iniciar una Live Activity simple, por ejemplo, desde tu aplicación, puedes usar el siguiente método en Flutter: defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)
Este método puede ser llamado en cualquier evento que desencadene el inicio de la Live Activity.
// Función para iniciar la Live Activity void startLiveActivity() { // Crea el ID de tu actividad String activityId = "stopwatch_activity";
// Define los atributos que quieres enviar a la Live Activity Map<String, dynamic> attributes = { 'title': 'Stopwatch Activity', 'description': 'This is a live activity for a stopwatch.' };
// Define el estado del contenido para actualizar en la Dynamic Island Map<String, dynamic> content = { 'elapsedSeconds': 0 };
// Llama al método defaultStart de Pushwoosh para activar la Live Activity Pushwoosh.getInstance().defaultStart(activityId, attributes, content); }