迁移不是翻译语法
“心晴手记”最初已经有 iOS 版本,随后使用 ArkTS + ArkUI 实现 HarmonyOS 原生版。
两个版本的产品目标一致:
- 每天记录一种心情;
- 增加情境标签和一句话日记;
- 完成或取消习惯打卡;
- 通过月历和 7/30 天统计回顾;
- 本地保存,不要求账号;
- 支持应用锁和数据导出。
但平台实现并不是一一对应的语法替换:
| iOS 思路 | HarmonyOS 实现 |
|---|---|
| Swift / SwiftUI | ArkTS / ArkUI |
| SwiftData | Preferences JSON |
| Face ID / Touch ID 场景 | UserAuthenticationKit 的 PIN/人脸/指纹 |
| Share Sheet / 文件导出 | CoreFileKit DocumentViewPicker |
| App 生命周期 | Stage 模型 UIAbility + 页面生命周期 |
| Localizable 资源 | AppScope 与模块区域资源 |
真正需要迁移的是业务语义,而不是 API 名字。
一、先列功能对照表,不要先重画首页
迁移前,我先建立功能对照:
iOS 功能 → HarmonyOS 目标 → 状态 → 验收路径
例如:
每日记录去重
→ dayIdentifier 更新同日记录
→ 已完成
→ 当天保存两次,确认只有一条记录
这张表的价值是把“页面看起来差不多”转换成可验证的产品行为。
如果直接从 UI 开始,最容易漏掉的是:
- 删除习惯时是否级联删除打卡;
- 归档后历史是否保留;
- 没有心情但有打卡的日期是否出现在历史;
- 语言切换后默认习惯是否变化;
- 撤回隐私同意后应用锁是否关闭。
这些行为往往比圆角和间距更决定迁移质量。
二、从“视图对象”转向明确的状态边界
SwiftUI 和 ArkUI 都是声明式 UI,但状态观察机制、对象更新方式和组件复用细节并不完全相同。
HarmonyOS 版本没有把所有状态藏在界面内部,而是先定义持久化根对象:
export interface PersistedState {
exportVersion: number;
settings: AppSettings;
moodEntries: MoodEntry[];
habits: Habit[];
habitCompletions: HabitCompletion[];
}
页面编辑的仍然是 ArkUI 状态,但保存时会生成完整快照交给 Repository。
这种做法可以清楚回答:
- 哪些值只是临时输入;
- 哪些值必须持久化;
- 哪些值可以从其他数据派生;
- 导出文件应该包含什么;
- 新版本如何给旧字段补默认值。
迁移时先建立状态边界,比寻找“Swift 某个属性包装器对应 ArkTS 哪个装饰器”更稳妥。
三、不要假设同为声明式 UI,Key 的行为就完全相同
项目早期出现过:习惯已经编辑,ArkUI 列表仍显示旧图标;切换 Tab 后又恢复正确。
数据没有错,问题在节点复用。最终通过不可变更新和包含修订信息的 Key 解决:
ForEach(this.activeHabits(), (habit: Habit) => {
// 构建习惯行
}, (habit: Habit) =>
`${habit.id}-${habit.updatedAtMillis}-${this.uiRevision}`
)
迁移经验是:
- 不要把另一个框架的刷新直觉直接套过来;
- 先确认数组和对象是否真正替换;
- 再检查 ArkUI Key 是否表达了显示身份;
- 不要用随机 Key 粗暴关闭全部复用。
相似的语法会让人降低警惕,但真正需要理解的是目标框架的状态与渲染模型。
四、平台能力要做“等价设计”,不是寻找同名 API
iOS 上的 Face ID 应用锁,迁移目标不应该写成“在 HarmonyOS 找 Face ID”。
真正的用户需求是:
使用设备已有的可信认证,保护本机日记入口。
因此 HarmonyOS 版本使用 UserAuthenticationKit,允许系统根据设备能力提供 PIN、人脸或指纹:
authType: [
userAuth.UserAuthType.PIN,
userAuth.UserAuthType.FACE,
userAuth.UserAuthType.FINGERPRINT
]
同样,iOS 分享/导出能力在 HarmonyOS 上通过系统文件 Picker 实现。用户需求仍然是“把数据保存到自己选择的位置”,不需要强行复制另一平台的弹窗样式。
迁移时应当写两列:
原平台 API 做了什么
用户真正需要什么
然后用目标平台最自然的系统能力满足第二列。
五、日期模型不能沿用“时间戳万能”的习惯
日记中的“一天”是本地日历概念。HarmonyOS 版本保存:
dayIdentifier: '2026-08-21'
而不是用 UTC 日期字符串充当业务日期。
export function dayIdentifier(date: Date): string {
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1)
.toString().padStart(2, '0');
const day = date.getDate()
.toString().padStart(2, '0');
return `${year}-${month}-${day}`;
}
这条规则必须在迁移初期确定,否则今日页、月历、历史和统计可能各自采用不同日期口径。
跨平台一致不等于两个平台保存完全相同的底层时间对象,而是用户在相同本地日期看到相同记录。
六、多语言要迁移稳定身份,不要迁移当前翻译
iOS 版本已有中英日文案,HarmonyOS 版本仍需要重新处理资源目录和应用偏好语言。
更重要的是,默认习惯不能把“早睡”这个中文字符串当作永久身份。
{
resourceName: 'starter_sleep',
storedName: 'sleep-early',
legacyNames: ['早睡', 'Sleep early', '早寝']
}
storedName是跨语言稳定身份;resourceName决定当前显示;legacyNames识别早期版本保存过的翻译文本。
迁移时,如果只复制用户当前看到的字符串,后续语言切换和旧数据兼容会非常困难。
七、迁移完成的标准是验收闭环,不是编译通过
项目最终建立了多层验证:
- ArkTS 编译通过;
- HAP/APP 构建成功;
- 模拟器完成首次隐私、引导和五个 Tab 抽查;
- 中英日切换及冷启动持久化通过;
- 今日、历史、习惯和统计按清单回归;
- 应用锁和文件保存器留给真机重点验证;
- AppGallery 发布资料单独检查。
特别要区分:
代码路径已实现 ≠ 系统能力已真机验收
设备认证、文件 URI、宽屏布局等能力,即使模拟器中可编译,也不能跳过真机验证。
一套可复用的迁移顺序
如果重新做一次,我仍会使用下面的顺序:
第 1 步:冻结源版本功能范围
不要一边迁移一边无限增加新功能。
第 2 步:提取业务不变量
日期唯一性、关联删除、归档语义、隐私边界先写清楚。
第 3 步:建立目标平台骨架
完成 Stage 模型、资源、模块配置和持久化入口。
第 4 步:按垂直切片实现
先走通“记录 → 保存 → 历史查看”,再扩展统计和设置。
第 5 步:替换为目标平台能力
系统认证、Picker、语言偏好使用 HarmonyOS 原生方案。
第 6 步:建立差异清单
明确哪些功能等价完成、哪些暂不提供、哪些需要真机验证。
第 7 步:以用户任务验收
不要只按文件和 API 测试,要按真实操作路径回归。
总结
从 SwiftUI 到 ArkUI,最重要的不是学会另一套组件语法,而是改变迁移思路:
- 迁移业务语义,不逐行翻译;
- 先定义状态和数据不变量;
- 尊重 ArkUI 的状态与复用模型;
- 为系统能力做等价设计;
- 日期和语言保存稳定身份;
- 用功能对照和 QA 清单闭环;
- 把“待真机验证”明确记录下来。
做到这些,跨平台重写才不会变成一个外观相似、行为却逐渐偏离的复制品。
本文案例来自“心晴手记(MoodMemoir)”iOS 与 HarmonyOS 双平台实现。

3653

被折叠的 条评论
为什么被折叠?



