# Live Activity iOS ในโปรเจกต์ Flutter

เราจะสร้าง Live Activity ที่มีการใช้งานนาฬิกาจับเวลาสำหรับโปรเจกต์ Flutter โปรเจกต์นี้จะรวมถึงการใช้งานฝั่ง Flutter และการใช้งานแบบเนทีฟในโปรเจกต์ Xcode

## การใช้งานฝั่ง Flutter
เราจะสร้างคลาสที่จะใช้งานสามเมธอด:
- `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);
    }
  }
}
```
เราจะสร้างแอปนาฬิกาจับเวลา และเพื่อส่งข้อมูลไทม์เมอร์ของนาฬิกาจับเวลาไปยังเมธอดเนทีฟ เราจำเป็นต้องสร้างโมเดลข้อมูลและส่งไปยังการเรียกใช้ method channel ในรูปแบบ 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'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
```
## การใช้งานฝั่งเนทีฟ

### การกำหนดค่าโปรเจกต์ Xcode

1. เปิดโปรเจกต์ของคุณบน Xcode และในแท็บ General ตั้งค่า minimum deployment เป็น iOS 16.1
<img src="/flutter-flutter-ios-live-activities-1.webp" alt="แท็บ General ของ Xcode แสดงการตั้งค่า minimum deployment เป็น iOS 16.1"/>

2. ใน `Info.plist` เพิ่มคีย์ใหม่ `NSSupportsLiveActivities` และตั้งค่าเป็น `YES` (Boolean)

### Info.plist
```xml
<key>NSSupportsLiveActivities</key>
<true/>
```
3. จากแถบ action เลือก **File > New > Target** และค้นหา `WidgetExtensionand` แล้วสร้างวิดเจ็ตใหม่

## การใช้งานฝั่งโค้ด
มาสร้างคลาสชื่อ 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` (ดังที่แสดงใน snippet) struct attribute นี้ทำหน้าที่เป็นโมเดลข้อมูลที่เก็บข้อมูลที่จะแสดงบน UI (โดยตรงจาก Flutter) และยังสามารถแก้ไข UI ได้ตามต้องการ

**Swift**

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

    public struct ContentState: Codable, Hashable {
        var elapsedTime: Int
    }
}
```
ตอนนี้ สิ่งเดียวที่เหลือคือ UI สำหรับ Dynamic Island! (เย้ เรามาไกลขนาดนี้แล้ว!) UI ทั้งหมดสำหรับ Dynamic Island จะต้องสร้างโดยใช้ SwiftUI สำหรับบทความนี้ ได้มีการออกแบบ UI แบบง่ายๆ (แต่คุณสามารถปรับแต่งได้ตามต้องการ)

โค้ด UI นี้ควรเขียนไว้ใน struct `StopwatchWidgetLiveActivity` ลบโค้ดที่มีอยู่ออกจาก struct และคุณสามารถทำตามโค้ดด้านล่างนี้:

**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
ปลั๊กอิน 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 และโทเค็นในฝั่ง 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](/th/developer/api-reference/ios-live-activities-api)

หากคุณต้องการเริ่ม Live Activity แบบง่ายๆ เช่น จากแอปของคุณ คุณสามารถใช้เมธอดต่อไปนี้ใน Flutter: `defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)`

เมธอดนี้สามารถเรียกใช้ได้ในทุก event ที่กระตุ้นการเริ่มต้นของ 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);
  }
```