ข้ามไปยังเนื้อหา

การใช้งาน 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'),
),
],
),
],
),
),
);
}
}

การใช้งานฝั่งเนทีฟ

Anchor link to

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

Anchor link to
  1. เปิดโปรเจกต์ของคุณบน Xcode และในแท็บ General ตั้งค่า minimum deployment เป็น iOS 16.1
Xcode General Tab showing minimum deployment set to iOS 16.1
  1. ใน Info.plist เพิ่มคีย์ใหม่ NSSupportsLiveActivities และตั้งค่าเป็น YES (Boolean)

Info.plist

Anchor link to
<key>NSSupportsLiveActivities</key>
<true/>
  1. จาก action bar เลือก File > New > Target และค้นหา WidgetExtension และสร้างวิดเจ็ตใหม่

การใช้งานฝั่งโค้ด

Anchor link to

มาสร้างคลาสชื่อ LiveActivityManager ที่จะจัดการ live activities สำหรับ Dynamic Island ของเรา คลาสนี้จะประกอบด้วยสามเมธอด: startLiveActivity(), updateLiveActivity() และ stopLiveActivity()

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) 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 code ด้วย

AppDelegate.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

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"});
/**
* เรียกใช้เมธอดนี้ `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`
"content-state": {
"data": { // คุณต้องส่งพารามิเตอร์ใน dictionary ด้วยคีย์ 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

// ฟังก์ชันสำหรับเริ่ม Live Activity
void startLiveActivity() {
// สร้าง ID กิจกรรมของคุณ
String activityId = "stopwatch_activity";
// กำหนด attributes ที่คุณต้องการส่งไปยัง Live Activity
Map<String, dynamic> attributes = {
'title': 'Stopwatch Activity',
'description': 'This is a live activity for a stopwatch.'
};
// กำหนด content state เพื่ออัปเดตบน Dynamic Island
Map<String, dynamic> content = {
'elapsedSeconds': 0
};
// เรียกเมธอด defaultStart ของ Pushwoosh เพื่อกระตุ้น Live Activity
Pushwoosh.getInstance().defaultStart(activityId, attributes, content);
}