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

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
  1. เปิดโปรเจกต์ของคุณใน Xcode และในแท็บ General ตั้งค่า minimum deployment เป็น iOS 16.1
  1. ใน Info.plist เพิ่มคีย์ใหม่ NSSupportsLiveActivities และตั้งค่าเป็น YES (Boolean)

Info.plist

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

การติดตั้งฝั่งโค้ด

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 (ดังที่แสดงในตัวอย่างโค้ด) 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 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"});
/**
* 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);
}