突然接到一个需求,需要我们在 IOS APP 中添加 widget 小组件,用来展示项目项目数据信息。大领导的需求没法拒绝,只能摸着石头过河,开干!
环境安装
由于项目用的是 Flutter 来搭建的,所以需要申请台 mac 电脑安装一遍开发环境。具体的准备我之前写过一篇 前端角度快速理解 Flutter 开发 的文章,我就不赘述了。
安装完各种环境就花了我大半天的时间,像 Android 的很多东西都需要科学上网后用 Android Studio 慢慢下载。
在执行 flutter doctor
命令显示 flutter 开发环境无误后,终于可以愉快地开发了。
解决项目老旧无法运行的问题
但是!现有 Flutter 项目已经有一两年没有维护了,无论是 Dart 还是各种三方库都特别老,和我安装的崭新环境格格不入......
就比如:
- SDK 版本太新,需要同步 yaml 配置项
sdk: ">=3.1.5 <4.0.0"
- 新的 Dart 语法对一些模糊写法会报错,变得更加严格。像
String tag;
这种只定义了类型,没有定义具体值的情况,新的 Dart 需要让我们加上 late 关键字表示这个变量会稍后赋值late String tag
。 - 第三方库弃用了某些 API,或者更换了某些 API 的位置,需要重新调整写法。比如 dio 库!
- Flutter 自身的界面组件(它称之为 widget)也有一些弃用的部分,比如 ElevatedButton。需要找新的 widget 替换。
这里只能一个一个问题的解决,这里有个小心得是:对这种一大堆问题的项目,可以用 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 和移动开发算是全新的领域,所以还是需要学不少知识点的。类比前端来说就是:
- 第三方库管理工具 ------ 这个其实在处理环境问题的时候逐渐就琢磨出来了。
- 常用的三方库 ------ 一些好用的轮子的使用是必须的,这个只要 README 写得好,照着文档慢慢摸索就可以了。
- 界面搭建 ------ 对于前端而言,无论是 flutter、小程序、react native、uniapp,其实本质都是界面渲染和逻辑处理,所以万变不离其宗,都是差不多的。只要认真看他们的官方文档教程就可以理解,就比如 Introducing SwiftUI | Apple Developer Documentation。在经历过各种前端框架的洗礼后我上手还是很快的。
- 编程语言 ------ 这次用到的 Swift、Dart、Java 其实设计思路上都很类似,所以上手很快。先是在菜鸟教程和官方文档中过了一遍基础语法,然后边学边查边写项目。
总之,这次新技术学习下来,发现在大目标都是界面呈现和逻辑处理的前提下,各方面其实都是可以和前端开发类比着用的。
而对于新技术很多不知道怎么写的问题(一般都是某些界面或者语言的写法不明确,都是很基础的东西),往往问 ChartGPT 要比 Google 有用的多,只要语言描述的足够精确就可以给到想要的代码。与时俱进嘛,AI 还是很好用的。
需求方案的选择
既然项目跑起来了,新技术知识学习好了,就得实现数据展示小组件啦~
方案调研下来有几种:
- 方案1,在 Flutter 侧使用 Canvas 将图形绘制出来并转为图片,然后通过 home_widget 将图片传到 IOS 侧渲染。
- 方案2,在 Flutter 侧通过接口获取数据 JSON,然后通过 home_widget 传给 IOS,最后由 IOS 端进行原生 Chart 渲染。
- 方案3,完全抛开 Flutter 侧,直接在 IOS 的 Widget Extension 模块实现接口请求、JSON 转换、数据渲染一条龙服务。
其中,方案 1 和方案 2 都需要让项目后台运行,所以要通过 workmanager 库进行后台数据更新。 但是吧......这个 workmanager 库怎么调试都不生效,查了 issue 发现好多人也有类似问题。然后去查看他的源码,发现源码中的 API 和 README 文档都对不上。调试一天没反应后只能放弃这个方案。
转而试了方案 3 发现表现良好,IOS 大概每过 5 分钟会发起一次从请求到渲染的过程。这个时间是系统定的,想来是为了不让开发者瞎搞影响手机能耗吧。
所以,最终选择了方案 3 的 IOS 一条龙服务。
业务需求
需求很简单
- 创建 IOS Widget Extension
- 获取用户信息
- 根据用户信息通过接口获取项目数据,IOS 的接口请求我用的是
URLSession.shared.dataTask(with: request) {}
- 将请求响应数据转为 JSON,这里用到了 SwiftyJSON。
- 将数据渲染为界面。
- 接口请求定时更新,由于 IOS Widget 的
getSnapshot
方法会由系统自动定时更新,所以开发者不需要考虑这方面。
Widget Extension 的创建
参考 iOS14 Widget小组件开发(Widget Extension) - 掘金 一文,就不多赘述了。
用户信息的获取
由于这个数据传输是需要权限,且根据个人展示不同数据的。所以,需要拿到用户数据才行。既然是 Flutter 和 Native 端的通信,用的就是 home_widget
模块了。大致原理我猜是两端共享了一块存储数据的空间。
另外,在 IOS 中需要将数据从 APP 端共享给 widget extension 端需要项目打开 APP GROUPS 功能,并且在 XCode 的 signing & Capabilities
中进行配置。
先定义一个工具类:
dart
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
dart
WidgetExtension.init();
在用户信息更新后让 home_widget 同步更新数据给 IOS(在项目中是将用户信息存在 SharedPreferences 中的)。
dart
WidgetExtension.updateWidgetData();
然后在 IOS widget 端获取用户信息
swift
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 Swift UI Charts 绘制
一开始,以我前端开发的思维,立马开始寻找 IOS 方面的 Chart 库。结果发现这些 Chart 库都是用在 UI Kit 界面体系下的。
而 Widget Extension 使用的是 Swift UI 界面来写的。所以这条路直接堵死。好在我后来发现 Swift UI 在 IOS 16.0+ 新增了 Swift Charts 模块。
下面是我写的 ChartView 组件,传入具体数据就可以展示 Chart 内容。
swift
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
}
}
}
感觉写法上很......业余,勉强能实现需求罢了。
收获
总结上面的过程,列一些小收获。
- 同一方向的技术是可以类比的,很多编程思路和设计都是通用的。只要某一项技术用的很熟练扎实,那么切换到别的技术就很快。
- 遇到复杂难题,可以通过 git 版本控制来定格每一次改动。方便追踪问题。
- 在查代码写法这方面,ChatGPT 类的工具真的香,提供一段描述就可以给出一段比较靠谱的代码。不过有的 GPT 有点过时了,应该是模型没更新的缘故。
- 像各类第三方库的问题,StackOverfow 和 Github Issue 是解决问题成功率最高的地方......国内的话也就掘金、SF 靠谱点了吧。CSDN 能把人气死......
- 报错类问题,也是建议直接贴到 Google 上,然后一个一个方案的去试。虽然费时但是有效。别钻牛角尖。
- 另外,结识一些不同领域的朋友也很棒。好多 IOS 的难以描述的问题,我只要一个截图过去朋友基本就能知道怎么回事。省时省力。