在Flutter專案中開發IOS桌面元件(WidgetExtension)
具體的WidgetExtension的開發流程這裡就不細說了,可以參考文末的連結。
在Flutter專案開發IOSWidget的過程中,主要的問題有:
- App和Widget的資料共享
- 點選Widget跳轉App的指定介面
- 在App介面編輯並更新Widget資料
App和Widget資料共享
資料共享使用的是UserDefaults,前提是需要為WidgetExtension和Runner新增相同的AppGroup。新增AppGroup的方法為:
Runner -> Target -> Runner -> Signing&Capabilities -> AppGroups -> +
這裡如果沒有AppGroups可以XCode點選右上角的+號來新增AppGroups。
WidgetExtension新增方法同上,其中AppGroup要和Runner的相同。
UserDefaults的使用
這裡以實際的例子為大家展示UserDefaults的使用。為了方便演示,在App啟動時儲存相關資料,以供小元件進行讀取。
// 以下程式碼在AppDelegate.swift中的Application方法中
// suitName: 為上面新增的AppGroup
let userDefaults = UserDefaults.init(suiteName: "group.com.cc.ToDo")
userDefaults!.setValue("defaultID", forKey: "id")
userDefaults!.setValue("defauleName", forKey: "name")
複製程式碼
在小元件中讀取
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
print("start getTimeline")
let userDefaults = UserDefaults(suiteName: "group.com.cc.ToDo")
let id = userDefaults?.string(forKey: "id")
let name = userDefaults?.string(forKey: "name")
print("timeline: \(id!) \(name!)")
// ... 這裡省略了後續的completion
}
複製程式碼
點選Widget跳轉App的指定介面
在小元件點選跳轉App這塊,本次使用的技術為widgetURL以及URL Schemes。
在小元件中處理點選跳轉主要有兩種方法:
- widgetURL:作用於整個小元件,且一個小元件只能有一個
- Link:作用於Link包裹的元件的大小,在小尺寸[systemSmall]元件中無法使用Link
可以根據實際情況選擇合適的元件。
URL Schemes
URL Schemes主要負責處理跳轉邏輯,通過配置URL Schemes,在App中捕獲對應的url和引數來實現跳轉指定頁。 註冊URL Schemes主要包含以下幾步:
Runner -> Info -> URL Types -> 新增+ -> 編輯URL Schemes
完成之後可以再widgetURL中新增url(以上述配置的URL Schemes開頭),程式碼如下:
var body: some View{
VStack{
Text("ToDoList")
Text(entry.userid)
Text(entry.author)
}
// URL以配置的URL Schemes開頭,可以拼接引數
.widgetURL(URL(string: "dynamictheme://user?userid=\(entry.userid)&author=\(entry.author)"))
}
// 在Link中配置的URL同此
複製程式碼
Flutter端則使用uni_links庫來進行連結捕獲和跳轉,具體實現如下:
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:uni_links/uni_links.dart';
import 'pages/UserPage.dart';
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
StreamSubscription<String> _sub;
@override
void initState() {
super.initState();
initPlatformStateForStringUniLinks();
}
Future<void> initPlatformStateForStringUniLinks() async {
String initialLink;
// App未開啟的狀態在這個地方捕獲scheme
try {
initialLink = await getInitialLink();
print('跳轉地址: $initialLink');
if (initialLink != null) {
print('跳轉地址不為null --$initialLink');
// 跳轉到指定頁面
schemeJump(context, initialLink);
}
} on PlatformException {
initialLink = 'Failed to get initial link.';
} on FormatException {
initialLink = 'Failed to parse the initial link as Uri.';
}
// App開啟的狀態監聽scheme
_sub = getLinksStream().listen((String link) {
if (!mounted || link == null) return;
print('link--$link');
// 跳轉到指定頁面
schemeJump(context, link);
}, onError: (Object err) {
if (!mounted) return;
});
}
void schemeJump(BuildContext context, String schemeUrl) {
final Uri _jumpUri = Uri.parse(schemeUrl.replaceFirst(
'dynamictheme://',
'http://path/',
));
switch (_jumpUri.path) {
case '/user':
print("接收到的引數為:");
String userid = _jumpUri.queryParameters["userid"];
print(userid);
String author = _jumpUri.queryParameters["author"];
print(author);
Navigator.of(navigatorKey.currentContext).push(CupertinoPageRoute(
builder: (context) => UserPage(
userid: userid,
author: author,
)));
break;
default:
break;
}
}
@override
void dispose() {
super.dispose();
_sub.cancel();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
title: 'Flutter 與 IOS',
theme:
ThemeData(primarySwatch: Colors.blue, platform: TargetPlatform.iOS),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("HomePage")
),
body: Center(
child: Text("Home page"),
),
);
}
}
複製程式碼
在App介面編輯並更新Widget資料
在App編輯資料並更新widget功能中,通過MenthodChannel來實現。當編輯完資料,需要更新時,通過MethodChannel來呼叫原生方法,在原生方法中更新UserDefaults的資料,並返回結果給Flutter端。
資料更新完成並不會重新整理Widget,因為Widget中使用的是前一Timeline的快照,在下一個Timeline之前並不會重新整理資料,因此需要主動呼叫相關方法來更新資料。
在原生端想要主動來更新小元件的Timeline,主要有兩種方法:
- WidgetCenter.shared.reloadAllTimelines(): 跟新App下所有元件的Timelines
- WidgetCenter.shared.reloadTimelines(ofKind: kind): 更新指定kind型別元件的Timelines
具體的實現如下可參考以下程式碼
Flutter端程式碼如下:
MethodChannel channel = MethodChannel("com.cc.ToDo.widgets");
var res = await channel.invokeMethod("updateWidgetData", {
"userid":idController.text,
"author":nameController.text
});
print(res);
print(res.runtimeType);
複製程式碼
Swift端程式碼如下:
import UIKit
import Flutter
import WidgetKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller:FlutterViewController = window?.rootViewController as! FlutterViewController
let userDefaults = UserDefaults.init(suiteName: "group.com.cc.ToDo")
userDefaults!.setValue("defaultID", forKey: "userid")
userDefaults!.setValue("defauleName", forKey: "author")
// 初始化MethodChannel,設定監聽
WidgetMenthod.init(messger: controller.binaryMessenger)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
複製程式碼
// 處理Flutter呼叫
class WidgetMenthod{
init(messger:FlutterBinaryMessenger){
let channel = FlutterMethodChannel(name: "com.cc.ToDo.widgets", binaryMessenger: messger)
channel.setMethodCallHandler{(call:FlutterMethodCall, result: @escaping FlutterResult) in
// 通過call.method來判斷要呼叫的方法
if(call.method == "updateWidgetData"){
// 通過call.arguments來獲取引數
if let dict = call.arguments as? Dictionary<String,Any>{
let userid = dict["userid"] as? String
let author = dict["author"] as? String
print("\(userid) ==== \(author)")
let userDefaults = UserDefaults.init(suiteName: "group.com.cc.ToDo")
userDefaults!.setValue(userid, forKey: "userid")
userDefaults!.setValue(author, forKey: "author")
if #available(iOS 14.0, *) {
print("reload timelines")
WidgetCenter.shared.reloadTimelines(ofKind: "todo_list")
print("reload complete!")
result(["code":1,"msg":"success"])
} else {
result(["code":0,"msg":"系統版本過低"])
}
}else{
result(["code":0,"msg":"引數異常"])
}
}
}
}
}
複製程式碼
至此,在Flutter專案中開發IOS桌面元件就全部完成了。