Live Activity iOS في مشروع Flutter
سنقوم بإنشاء Live Activity مع تنفيذ ساعة توقيت (Stopwatch) لمشروع Flutter. سيتضمن المشروع تنفيذًا من جانب Flutter بالإضافة إلى تنفيذ أصلي في مشروع Xcode.
التنفيذ من جانب Flutter
Anchor link toسنقوم بإنشاء فئة ستنفذ ثلاث دوال:
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); } }}سنقوم ببناء تطبيق ساعة توقيت، ولتمرير بيانات مؤقت ساعة التوقيت إلى الدوال الأصلية، نحتاج إلى إنشاء نموذج بيانات وإرساله إلى استدعاء قناة الدالة (method channel) بتنسيق خريطة شبيه بـ JSON.
class DynamicIslandStopwatchDataModel { final int elapsedSeconds;
DynamicIslandStopwatchDataModel({ required this.elapsedSeconds, });
Map<String, dynamic> toMap() { return <String, dynamic>{ 'elapsedSeconds': elapsedSeconds, }; }}ستتضمن كل دالة الكود اللازم لاستدعاء الدوال الأصلية المقابلة.
كان هذا هو الإعداد الأولي المطلوب لتشغيله على جانب Flutter. بعد ذلك، تم تنفيذ واجهة مستخدم أساسية لتطبيق ساعة توقيت مع استدعاءات الدوال اللازمة.
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'), ), ], ), ], ), ), ); }}التنفيذ من الجانب الأصلي
Anchor link toإعداد مشروع Xcode
Anchor link to- افتح مشروعك على Xcode وفي علامة التبويب General، قم بتعيين الحد الأدنى للنشر إلى iOS 16.1

- في
Info.plist، أضف مفتاحًا جديدًاNSSupportsLiveActivitiesواضبط قيمته علىYES(Boolean).
Info.plist
Anchor link to<key>NSSupportsLiveActivities</key><true/>- من شريط الإجراءات، حدد File > New > Target وابحث عن
WidgetExtensionandوأنشئ ويدجت جديد.
التنفيذ من جانب الكود
Anchor link toلنقم بإنشاء فئة تسمى LiveActivityManager ستقوم بإدارة الأنشطة الحية (live activities) لـ Dynamic Island الخاصة بنا. ستحتوي هذه الفئة على ثلاث دوال: startLiveActivity()، updateLiveActivity()، و 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)) } }}كل دالة لها وظيفتها المحددة. startLiveActivity() (كما يوحي الاسم) مسؤولة عن بدء Live Activity، مما يؤدي إلى تشغيل وظيفة Dynamic Island. وبالمثل، تتعامل stopLiveActivity() و updateLiveActivity() مع إيقاف Live Activity وتحديث البيانات المعروضة على Dynamic Island.
بعد ذلك، افتح StopWatchDIWidgetLiveActivity.swift وقم بتعديل بنية StopwatchDIWidgetAttributes (كما هو موضح في المقتطف). تعمل بنية السمات هذه كنموذج بيانات يحتفظ بالمعلومات التي سيتم عرضها على واجهة المستخدم (مباشرة من Flutter) ويمكنها أيضًا تعديل واجهة المستخدم حسب الحاجة.
Swift
struct StopwatchWidgetAttributes: ActivityAttributes { public typealias stopwatchStatus = ContentState
public struct ContentState: Codable, Hashable { var elapsedTime: Int }}الآن، الشيء الوحيد المتبقي هو واجهة المستخدم لـ Dynamic Island! (يا للروعة، لقد وصلنا إلى هذا الحد!) يجب إنشاء واجهة المستخدم الكاملة لـ Dynamic Island باستخدام SwiftUI. في هذه المقالة، تم تصميم واجهة مستخدم بسيطة (ولكن لا تتردد في تخصيصها حسب الحاجة).
يجب كتابة كود واجهة المستخدم هذا داخل بنية StopwatchWidgetLiveActivity. قم بإزالة الكود الحالي من البنية، ويمكنك اتباع الكود أدناه:
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) } }}لا تنسَ أيضًا تعديل كود 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) }}وهذا كل شيء! يغطي هذا كل ما هو مطلوب لتنفيذ Dynamic Island وربطها بكود Flutter!
تنفيذ كود Pushwoosh
Anchor link toيوفر ملحق Pushwoosh Flutter دالتين لإدارة Live Activity في المشروع.
/// 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.
استدعِ هذه الدالة أثناء تهيئة التطبيق.
Pushwoosh.initialize({"app_id": "XXXXX-XXXXX", "sender_id": "XXXXXXXXXXXX"});/*** Call this method `defaultSetup()`*/ Pushwoosh.getInstance.defaultSetup();أثناء تهيئة التطبيق، سيرسل Pushwoosh رمز push-to-start الخاص بك إلى الخادم، مما سيسمح لك لاحقًا ببدء Live Activity عبر استدعاء API.
لبدء Live Activity، تحتاج إلى إجراء طلب API. فيما يلي مثال على الطلب:
{ "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)
إذا كنت ترغب في بدء Live Activity بسيط، على سبيل المثال، من تطبيقك، يمكنك استخدام الدالة التالية في Flutter: defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)
يمكن استدعاء هذه الدالة عند أي حدث يؤدي إلى بدء Live Activity.
// 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); }