# Live Activity de iOS en un proyecto Flutter

Crearemos una Live Activity con una implementación de cronómetro para un proyecto de 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
Crearemos una clase que implementará tres métodos:
- `startLiveActivity()`
- `updateLiveActivity()`
- `stopLiveActivity()`.

```dart
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.

```dart
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.

```dart
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'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
```
## Implementación del lado nativo

### Configuración del proyecto de Xcode

1. Abra su proyecto en Xcode y en la pestaña General establezca el despliegue mínimo en iOS 16.1
<img src="/flutter-flutter-ios-live-activities-1.webp" alt="Pestaña General de Xcode mostrando el despliegue mínimo establecido en iOS 16.1"/>

2. En `Info.plist`, agregue una nueva clave `NSSupportsLiveActivities` y establezca su valor en `YES` (Booleano).

### Info.plist
```xml
<key>NSSupportsLiveActivities</key>
<true/>
```
3. Desde la barra de acciones, seleccione **File > New > Target** y busque `WidgetExtensionand` para crear un nuevo widget.

## Implementación del lado del código
Vamos 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**

```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))
        }
    }
}
```

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, abra `StopWatchDIWidgetLiveActivity.swift` y modifique 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**

```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! (¡Bravo, 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éntase libre de personalizarla según sea necesario).

Este código de la interfaz de usuario debe escribirse dentro de la estructura `StopwatchWidgetLiveActivity`. Elimine el código existente de la estructura y puede seguir el código a continuación:

**SwiftUI**
```swift
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 olvide modificar también el `código de AppDelegate`.

**AppDelegate.swift**
```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)
    }
}
```
¡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
El plugin de Flutter de Pushwoosh proporciona dos métodos para gestionar la Live Activity en el proyecto.

```dart
  /// 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});
  }
```
El método `defaultSetup()` le permite gestionar la estructura de la Live Activity y los tokens del lado de Pushwoosh.

Llame a este método durante la inicialización de la aplicación.

```dart
  Pushwoosh.initialize({"app_id": "XXXXX-XXXXX"});
/**
* Call this method `defaultSetup()`
*/
  Pushwoosh.getInstance.defaultSetup();
```
Durante la inicialización de la aplicación, Pushwoosh enviará su token de push-to-start al servidor, lo que le permitirá más tarde iniciar la Live Activity a través de una llamada a la API.
Para iniciar la Live Activity, necesita hacer una solicitud a la API. A continuación se muestra un ejemplo de la solicitud:

```json
{
  "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"
      }
    ]
  }
}
```

[Documentación más detallada sobre la API](/es/developer/api-reference/ios-live-activities-api)

Si desea iniciar una Live Activity simple, por ejemplo, desde su aplicación, puede usar el siguiente método en Flutter: `defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)`

Este método se puede llamar en cualquier evento que active el inicio de la Live Activity.

```dart
// 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);
  }
```