突然接到一个需求,需要我们在 IOS APP 中添加 widget 小组件,用来展示项目项目数据信息。大领导的需求没法拒绝,只能摸着石头过河,开干!
由于项目用的是 Flutter 来搭建的,所以需要申请台 mac 电脑安装一遍开发环境。具体的准备我之前写过一篇 前端角度快速理解 Flutter 开发 的文章,我就不赘述了。
安装完各种环境就花了我大半天的时间,像 Android 的很多东西都需要科学上网后用 Android Studio 慢慢下载。
在执行 flutter doctor
命令显示 flutter 开发环境无误后,终于可以愉快地开发了。
但是!现有 Flutter 项目已经有一两年没有维护了,无论是 Dart 还是各种三方库都特别老,和我安装的崭新环境格格不入……
就比如:
sdk: ">=3.1.5 <4.0.0"
String tag;
这种只定义了类型,没有定义具体值的情况,新的 Dart 需要让我们加上 late 关键字表示这个变量会稍后赋值 late String tag
。这里只能一个一个问题的解决,这里有个小心得是:对这种一大堆问题的项目,可以用 git commit 来管理代码,解决一个问题就提交一次 commit,这样就可以把问题单独出来一个个解决。后续也可以知道每个问题的解决方法。像我一开始没有用 git 管理,问题改着改着就成了一团乱麻了。
处理完 Flutter 的问题还需要处理 IOS 部分。
首先,想要运行 IOS 项目在真机开发室需要证书的,所以找到 IOS 相关的同事要到证书,我拿到的证书有 dev、inhouse、p12 三个,双击将证书都安装到 mac 电脑上。在项目调试的时候使用的是 dev 证书,而项目发版的时候需要用 inhouse 证书。
然后,老的 IOS 项目也是无法在新的 XCode 中运行的,需要用 CocoaPod 升级各种依赖库,将支持的 IOS 版本改为 16.0+,
另外还会遇到不少奇怪的报错。总之就是看到报错就贴到 Google 和 ChatGPT 上找解决方案,虽然费力了点,但一步步的总还是能够解决的。
经过一两天的问题修复后,连上 iphone 运行 flutter run
命令,终于……项目正常跑了起来。
由于 flutter 和移动开发算是全新的领域,所以还是需要学不少知识点的。类比前端来说就是:
总之,这次新技术学习下来,发现在大目标都是界面呈现和逻辑处理的前提下,各方面其实都是可以和前端开发类比着用的。
而对于新技术很多不知道怎么写的问题(一般都是某些界面或者语言的写法不明确,都是很基础的东西),往往问 ChartGPT 要比 Google 有用的多,只要语言描述的足够精确就可以给到想要的代码。与时俱进嘛,AI 还是很好用的。
既然项目跑起来了,新技术知识学习好了,就得实现数据展示小组件啦~
方案调研下来有几种:
其中,方案 1 和方案 2 都需要让项目后台运行,所以要通过 workmanager 库进行后台数据更新。
但是吧……这个 workmanager 库怎么调试都不生效,查了 issue 发现好多人也有类似问题。然后去查看他的源码,发现源码中的 API 和 README 文档都对不上。调试一天没反应后只能放弃这个方案。
转而试了方案 3 发现表现良好,IOS 大概每过 5 分钟会发起一次从请求到渲染的过程。这个时间是系统定的,想来是为了不让开发者瞎搞影响手机能耗吧。
所以,最终选择了方案 3 的 IOS 一条龙服务。
需求很简单
URLSession.shared.dataTask(with: request) {}
getSnapshot
方法会由系统自动定时更新,所以开发者不需要考虑这方面。参考 iOS14 Widget小组件开发(Widget Extension) - 掘金 一文,就不多赘述了。
由于这个数据传输是需要权限,且根据个人展示不同数据的。所以,需要拿到用户数据才行。既然是 Flutter 和 Native 端的通信,用的就是 home_widget
模块了。大致原理我猜是两端共享了一块存储数据的空间。
另外,在 IOS 中需要将数据从 APP 端共享给 widget extension 端需要项目打开 APP GROUPS 功能,并且在 XCode 的 signing & Capabilities
中进行配置。
先定义一个工具类:
import 'package:home_widget/home_widget.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:xdata3/utils/constants.dart';
const String appGroupId = 'YOUR APP GROUP ID'; // 这里就是 APP GROUP ID
const String iOSWidgetName = 'GDataWidget';
class WidgetExtension {
// 设置 APP GROUP ID
static void init() {
HomeWidget.setAppGroupId(appGroupId);
}
static void updateWidgetData() async {
final now = DateTime.now();
SharedPreferences prefs = await SharedPreferences.getInstance();
String userName = prefs.getString(SharePreferenceKey.USER_NAME) ?? "";
String tokenLocal =
prefs.getString(SharePreferenceKey.TOKEN_PREFIX + userName) ?? "";
HomeWidget.saveWidgetData("XDATA_USER_NAME", userName);
HomeWidget.saveWidgetData<String>('XDATA_TOKEN', tokenLocal);
HomeWidget.saveWidgetData<String>('XDATA_TIME',
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}');
HomeWidget.updateWidget(
iOSName: iOSWidgetName,
);
}
}
在 Flutter 项目初始化的时候初始化 home_widget
WidgetExtension.init();
在用户信息更新后让 home_widget 同步更新数据给 IOS(在项目中是将用户信息存在 SharedPreferences 中的)。
WidgetExtension.updateWidgetData();
然后在 IOS widget 端获取用户信息
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
print("get snapshot")
let userDefaults = UserDefaults(suiteName: "YOUR APP GROUP ID")
let username = userDefaults?.string(forKey: "XDATA_USER_NAME") ?? ""
let token = userDefaults?.string(forKey: "XDATA_TOKEN") ?? ""
// other logic
}
一开始,以我前端开发的思维,立马开始寻找 IOS 方面的 Chart 库。结果发现这些 Chart 库都是用在 UI Kit 界面体系下的。
而 Widget Extension 使用的是 Swift UI 界面来写的。所以这条路直接堵死。好在我后来发现 Swift UI 在 IOS 16.0+ 新增了 Swift Charts 模块。
下面是我写的 ChartView 组件,传入具体数据就可以展示 Chart 内容。
import SwiftUI
import Charts
import SwiftyJSON
struct DataInfo: Identifiable {
var legend: String
var x: String
var y: Double
var id = UUID()
}
struct ChartView: View {
private var propInfos: [DataInfo] = []
@State private var infos: [DataInfo] = []
init(propInfos: [DataInfo]) {
self.propInfos = propInfos
}
var body: some View {
Chart(infos) {
LineMark(x: .value("x", $0.x), y: .value("y", $0.y))
.foregroundStyle(by: .value("legend", $0.legend))
.accessibilityHidden(true)
.mask{ RectangleMark() }
RuleMark(y: .value("zero line", 0))
.foregroundStyle(.gray)
.lineStyle(StrokeStyle(dash: [2, 2]))
}
.chartForegroundStyleScale([
"今天": Color(red: 255/255, green: 55/255, blue: 38/255),
"昨天": Color(red: 190/255, green: 192/255, blue: 199/255)
])
.chartYAxis {
AxisMarks(position: .leading) { _ in
}
}
.chartXAxis {
AxisMarks(position: .bottom) { _ in
}
}
.chartLegend(.hidden)
.frame(width: 60, height: 32)
.onAppear{
self.infos = self.propInfos
}
}
}
感觉写法上很……业余,勉强能实现需求罢了。
总结上面的过程,列一些小收获。