# Live Activity iOS في مشروع Flutter

سنقوم بإنشاء Live Activity مع تنفيذ ساعة توقيت (Stopwatch) لمشروع Flutter. سيشمل المشروع تنفيذًا من جانب Flutter بالإضافة إلى تنفيذ أصلي (native) في مشروع Xcode.

## التنفيذ من جانب Flutter
سنقوم بإنشاء فئة (class) ستنفذ ثلاث دوال (methods):
- `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);
    }
  }
}
```
سنقوم ببناء تطبيق ساعة توقيت، ولتمرير بيانات مؤقت ساعة التوقيت إلى الدوال الأصلية (native methods)، نحتاج إلى إنشاء نموذج بيانات (data model) وإرساله إلى استدعاء قناة الدالة (method channel invocation) بتنسيق خريطة (map) شبيه بـ JSON.

```dart
class DynamicIslandStopwatchDataModel {
  final int elapsedSeconds;

  DynamicIslandStopwatchDataModel({
    required this.elapsedSeconds,
  });

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'elapsedSeconds': elapsedSeconds,
    };
  }
}
```
ستتضمن كل دالة الكود اللازم لاستدعاء الدوال الأصلية المقابلة.

كان هذا هو الإعداد الأولي المطلوب لتشغيله من جانب Flutter. بعد ذلك، تم تنفيذ واجهة مستخدم (UI) أساسية لتطبيق ساعة التوقيت مع استدعاءات الدوال اللازمة.

```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'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
```
## التنفيذ من جانب Native

### إعداد مشروع Xcode

1. افتح مشروعك على Xcode وفي علامة التبويب General، قم بتعيين الحد الأدنى للنشر (minimum deployment) إلى iOS 16.1
<img src="/flutter-flutter-ios-live-activities-1.webp" alt="علامة التبويب General في Xcode تظهر الحد الأدنى للنشر المحدد بـ iOS 16.1"/>

2. في `Info.plist`، أضف مفتاحًا جديدًا `NSSupportsLiveActivities` واضبط قيمته على `YES` (Boolean).

### Info.plist
```xml
<key>NSSupportsLiveActivities</key>
<true/>
```
3. من شريط الإجراءات، حدد **File > New > Target** وابحث عن `WidgetExtensionand` وأنشئ widget جديدًا.

## التنفيذ من جانب الكود
لنقم بإنشاء فئة تسمى LiveActivityManager ستدير الأنشطة الحية (live activities) لـ Dynamic Island الخاص بنا. ستحتوي هذه الفئة على ثلاث دوال: `startLiveActivity()`، `updateLiveActivity()`، و `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))
        }
    }
}
```

كل دالة لها وظيفتها المحددة. `startLiveActivity()` (كما يوحي الاسم) مسؤولة عن بدء Live Activity، مما يؤدي إلى تشغيل وظيفة Dynamic Island. وبالمثل، تتعامل `stopLiveActivity()` و `updateLiveActivity()` مع إيقاف Live Activity وتحديث البيانات المعروضة على Dynamic Island.

بعد ذلك، افتح `StopWatchDIWidgetLiveActivity.swift` وقم بتعديل بنية (struct) `StopwatchDIWidgetAttributes` (كما هو موضح في المقتطف). تعمل بنية السمات (attribute struct) هذه كنموذج بيانات (data model) يحتفظ بالمعلومات التي سيتم عرضها على واجهة المستخدم (UI) (مباشرة من Flutter) ويمكنها أيضًا تعديل واجهة المستخدم حسب الحاجة.

**Swift**

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

    public struct ContentState: Codable, Hashable {
        var elapsedTime: Int
    }
}
```
الآن، الشيء الوحيد المتبقي هو واجهة المستخدم (UI) لـ Dynamic Island! (يا للروعة، لقد وصلنا إلى هذا الحد!)
يجب إنشاء واجهة المستخدم بأكملها لـ Dynamic Island باستخدام SwiftUI. في هذا المقال، تم تصميم واجهة مستخدم بسيطة (ولكن لا تتردد في تخصيصها حسب الحاجة).

يجب كتابة كود واجهة المستخدم هذا داخل بنية `StopwatchWidgetLiveActivity`. قم بإزالة الكود الحالي من البنية، ويمكنك اتباع الكود أدناه:

**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)
        }
    }
}
```
لا تنسَ أيضًا تعديل كود `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)
    }
}
```
وهذا كل شيء! هذا يغطي كل ما هو مطلوب لتنفيذ Dynamic Island وربطه بكود Flutter!

## تنفيذ كود Pushwoosh
يوفر ملحق (plugin) Pushwoosh Flutter دالتين لإدارة Live Activity في المشروع.

```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});
  }
```
تسمح لك دالة `defaultSetup()` بإدارة بنية Live Activity والرموز (tokens) من جانب Pushwoosh.

استدعِ هذه الدالة أثناء تهيئة التطبيق.

```dart
  Pushwoosh.initialize({"app_id": "XXXXX-XXXXX"});
/**
* Call this method `defaultSetup()`
*/
  Pushwoosh.getInstance.defaultSetup();
```
أثناء تهيئة التطبيق، سيرسل Pushwoosh رمز البدء بالدفع (push-to-start token) الخاص بك إلى الخادم، مما سيسمح لك لاحقًا ببدء Live Activity عبر استدعاء API.
لبدء Live Activity، تحتاج إلى إجراء طلب API. فيما يلي مثال على الطلب:

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

[وثائق أكثر تفصيلاً حول واجهة برمجة التطبيقات (API)](/ar/developer/api-reference/ios-live-activities-api)

إذا كنت ترغب في بدء Live Activity بسيط، على سبيل المثال، من تطبيقك، يمكنك استخدام الدالة التالية في Flutter: `defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)`

يمكن استدعاء هذه الدالة عند أي حدث يؤدي إلى بدء 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);
  }
```