Live Activity iOS ในโปรเจกต์ Flutter
เราจะสร้าง Live Activity ด้วยการใช้งานนาฬิกาจับเวลาสำหรับโปรเจกต์ 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 ในรูปแบบ map ที่คล้าย JSON
class DynamicIslandStopwatchDataModel { final int elapsedSeconds;
DynamicIslandStopwatchDataModel({ required this.elapsedSeconds, });
Map<String, dynamic> toMap() { return <String, dynamic>{ 'elapsedSeconds': elapsedSeconds, }; }}
แต่ละเมธอดจะรวมโค้ดที่จำเป็นสำหรับการเรียกเมธอดเนทีฟที่เกี่ยวข้อง
นี่คือการตั้งค่าเบื้องต้นที่จำเป็นในการรันบนฝั่ง Flutter หลังจากนี้ ได้มีการติดตั้ง UI แอปนาฬิกาจับเวลาพื้นฐานพร้อมกับการเรียกเมธอดที่จำเป็น
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)
Anchor link toการกำหนดค่าโปรเจกต์ Xcode
Anchor link to- เปิดโปรเจกต์ของคุณใน Xcode และในแท็บ General ตั้งค่า minimum deployment เป็น 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
และแก้ไข struct StopwatchDIWidgetAttributes
(ดังที่แสดงในตัวอย่างโค้ด) attribute struct นี้ทำหน้าที่เป็นโมเดลข้อมูลที่เก็บข้อมูลที่จะแสดงบน UI (โดยตรงจาก Flutter) และยังสามารถแก้ไข UI ได้ตามต้องการ
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
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 token ของคุณไปยังเซิร์ฟเวอร์ ซึ่งจะช่วยให้คุณสามารถเริ่ม 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); }