在Flutter项目中开发IOS桌面组件(WidgetExtension)

·  阅读 1050

在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。

AppGroup.jpg 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的指定界面

在小组件中处理点击跳转主要有两种方法:

  • widgetURL:作用于整个小组件,且一个小组件只能有一个
  • Link:作用于Link包裹的组件的大小,在小尺寸[systemSmall]组件中无法使用Link

可以根据实际情况选择合适的组件。

URL Schemes

URL Schemes主要负责处理跳转逻辑,通过配置URL Schemes,在App中捕获对应的url和参数来实现跳转指定页。 注册URL Schemes主要包含以下几步:

Runner -> Info -> URL Types -> 添加+ -> 编辑URL Schemes

URLSchemes.jpg

完成之后可以再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桌面组件就全部完成了。

完整案例源码点此下载

参考文章

网易云音乐 iOS 14 小组件实战手册

【Flutter 混合开发】与原生通信-MethodChannel

分类:
前端
标签:
分类:
前端
标签: