# Live Activity iOS dans un projet Flutter

Nous allons créer une Live Activity avec une implémentation de chronomètre pour un projet Flutter. Le projet inclura une implémentation côté Flutter ainsi qu'une implémentation native dans le projet Xcode.

## Implémentation côté Flutter
Nous allons créer une classe qui implémentera trois méthodes :
- `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);
    }
  }
}
```
Nous allons construire une application de chronomètre, et pour transmettre les données du minuteur du chronomètre aux méthodes natives, nous devons créer un modèle de données et l'envoyer à l'invocation du canal de méthode dans un format de carte de type JSON.

```dart
class DynamicIslandStopwatchDataModel {
  final int elapsedSeconds;

  DynamicIslandStopwatchDataModel({
    required this.elapsedSeconds,
  });

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'elapsedSeconds': elapsedSeconds,
    };
  }
}
```
Chaque méthode inclura le code nécessaire pour appeler les méthodes natives correspondantes.

C'était la configuration initiale requise pour l'exécuter côté Flutter. Ensuite, une interface utilisateur basique d'application de chronomètre avec les appels de méthode nécessaires a été implémentée.

```dart
class _StopWatchScreenState extends State<StopWatchScreen> {
  int seconds = 0;
  bool isRunning = false;
  Timer? timer;

  /// la clé de canal est utilisée pour envoyer des données du côté Flutter vers le côté Swift via
  /// un pont unique (lien entre Flutter & Swift)
  final DynamicIslandManager diManager = DynamicIslandManager(channelKey: 'PW');

  void startTimer() {
    setState(() {
      isRunning = true;
    });

    // invocation de la méthode startLiveActivity
    diManager.startLiveActivity(
      jsonData: DynamicIslandStopwatchDataModel(elapsedSeconds: 0).toMap(),
    );

    timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        seconds++;
      });
            // invocation de la méthode updateLiveActivity
      diManager.updateLiveActivity(
        jsonData: DynamicIslandStopwatchDataModel(
          elapsedSeconds: seconds,
        ).toMap(),
      );
    });
  }

  void stopTimer() {
    timer?.cancel();
    setState(() {
      seconds = 0;
      isRunning = false;
    });

    // invocation de la méthode 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'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
```
## Implémentation côté natif

### Configuration du projet Xcode

1. Ouvrez votre projet sur Xcode et dans l'onglet Général, définissez le déploiement minimum sur iOS 16.1
<img src="/flutter-flutter-ios-live-activities-1.webp" alt="Onglet Général de Xcode montrant le déploiement minimum défini sur iOS 16.1"/>

2. Dans `Info.plist`, ajoutez une nouvelle clé `NSSupportsLiveActivities` et définissez sa valeur sur `YES` (Booléen).

### Info.plist
```xml
<key>NSSupportsLiveActivities</key>
<true/>
```
3. Dans la barre d'actions, sélectionnez **Fichier > Nouveau > Cible** et recherchez `WidgetExtensionand` puis créez un nouveau widget.

## Implémentation du code
Créons une classe appelée LiveActivityManager qui gérera les activités en direct pour notre Dynamic Island. Cette classe contiendra trois méthodes : `startLiveActivity()`, `updateLiveActivity()` et `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))
        }
    }
}
```

Chaque méthode a sa fonctionnalité spécifique. `startLiveActivity()` (comme son nom l'indique) est responsable du lancement de la Live Activity, ce qui déclenche la fonctionnalité Dynamic Island. De même, `stopLiveActivity()` et `updateLiveActivity()` gèrent l'arrêt de la Live Activity et la mise à jour des données affichées sur la Dynamic Island.

Ensuite, ouvrez `StopWatchDIWidgetLiveActivity.swift` et modifiez la structure `StopwatchDIWidgetAttributes` (comme montré dans l'extrait). Cette structure d'attributs sert de modèle de données qui contient les informations à afficher sur l'interface utilisateur (directement depuis Flutter) et peut également modifier l'interface utilisateur si nécessaire.

**Swift**

```swift
struct StopwatchWidgetAttributes: ActivityAttributes {
    public typealias stopwatchStatus = ContentState

    public struct ContentState: Codable, Hashable {
        var elapsedTime: Int
    }
}
```
Maintenant, il ne reste plus que l'interface utilisateur pour la Dynamic Island ! (Youhou, nous sommes arrivés jusqu'ici !)
L'ensemble de l'interface utilisateur pour la Dynamic Island doit être créé avec SwiftUI. Pour cet article, une interface utilisateur simple a été conçue (mais n'hésitez pas à la personnaliser selon vos besoins).

Ce code d'interface utilisateur doit être écrit à l'intérieur de la structure `StopwatchWidgetLiveActivity`. Supprimez le code existant de la structure, et vous pouvez suivre le code ci-dessous :

**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)
        }
    }
}
```
N'oubliez pas de modifier également le `code 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)
    }
}
```
Et c'est tout ! Cela couvre tout ce qui est nécessaire pour implémenter la Dynamic Island et la lier au code Flutter !

## Implémentation du code Pushwoosh
Le plugin Flutter de Pushwoosh fournit deux méthodes pour gérer la Live Activity dans le projet.

```dart
  /// Configuration par défaut de la Live Activity
  Future<void> defaultSetup() async {
    await _channel.invokeMethod("defaultSetup");
  }

  /// Démarrage par défaut de la Live Activity
  /// [activityId] ID de l'activité
  /// [attributes] attributs
  /// [content] contenu
  Future<void> defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content) async {
    await _channel.invokeMethod("defaultStart", {"activityId": activityId, "attributes": attributes, "content": content});
  }
```
La méthode `defaultSetup()` vous permet de gérer la structure de la Live Activity et les jetons côté Pushwoosh.

Appelez cette méthode lors de l'initialisation de l'application.

```dart
  Pushwoosh.initialize({"app_id": "XXXXX-XXXXX"});
/**
* Appelez cette méthode `defaultSetup()`
*/
  Pushwoosh.getInstance.defaultSetup();
```
Pendant l'initialisation de l'application, Pushwoosh enverra votre jeton de démarrage par push au serveur, ce qui vous permettra plus tard de démarrer la Live Activity via un appel API.
Pour démarrer la Live Activity, vous devez effectuer une requête API. Voici un exemple de la requête :

```json
{
  "request": {
    "application": "XXXXX-XXXXX",
    "auth": "YOUR_AUTH_API_TOKEN",
    "notifications": [
      {
        "content": "Message",
        "title":"Title",
        "live_activity": {
          "event": "start", // événement `start`
            "content-state": {
                "data": { // Vous devez passer les paramètres dans un dictionnaire avec la clé 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"
      }
    ]
  }
}
```

[Documentation plus détaillée sur l'API](/fr/developer/api-reference/ios-live-activities-api)

Si vous souhaitez démarrer une Live Activity simple, par exemple, depuis votre application, vous pouvez utiliser la méthode suivante dans Flutter : `defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)`

Cette méthode peut être appelée sur n'importe quel événement qui déclenche le démarrage de la Live Activity.

```dart
// Fonction pour démarrer la Live Activity
  void startLiveActivity() {
    // Créez votre ID d'activité
    String activityId = "stopwatch_activity";

    // Définissez les attributs que vous souhaitez envoyer à la Live Activity
    Map<String, dynamic> attributes = {
      'title': 'Stopwatch Activity',
      'description': 'This is a live activity for a stopwatch.'
    };

    // Définissez l'état du contenu à mettre à jour sur la Dynamic Island
    Map<String, dynamic> content = {
      'elapsedSeconds': 0
    };

    // Appelez la méthode defaultStart de Pushwoosh pour déclencher la Live Activity
    Pushwoosh.getInstance().defaultStart(activityId, attributes, content);
  }
```