阅读视图

发现新文章,点击刷新页面。

用自定义 Layout 化解 SwiftUI List 的行高与间距跳变

动画的声明式表达是 SwiftUI 的核心优势之一。但在某些场景里,结果并不总像我们期待的那样平滑。一个典型例子是:当 `List` 行内的内容高度发生动态变化——副标题从空变为非空、文本因更新而导致行数变化——系统自带的布局引擎往往无法给出连续的过渡动画。本文从这个现象出发,逐层拆解原因,给出一种完全基于 SwiftUI 原生能力的解决方案;也借这条路径回看 SwiftUI 在布局机制层面的几个关键约束。

从社区路标到生态基石:Dave Verwer 的新篇章 - 肘子的 Swift 周报 #137

Dave Verwer 在 iOS Dev Weekly 第 751 期宣布,这份已经持续近 15 年的周报将交由新的团队继续运营,而他自己接下来会全职投入 Swift Package Index。我的博客在早期获得关注,也曾得益于 iOS Dev Weekly 的推荐;而我在周报中坚持撰写每期周评,同样在很大程度上受到 Dave Verwer 的启发。对于很多 Apple 平台开发者来说,iOS Dev Weekly 早已不只是一份链接合集。它既是社区路标,也是长期陪伴。

消失的 WWDC 愿望单 - 肘子的 Swift 周报 #136

距离 WWDC 2026 只剩下 20 天了。每年到这个时候,我都会看到不少开发者分享自己的 WWDC 愿望单,写下预测与期许。但今年,至少到我汇总本期周报时,这类内容相较去年同期明显少了许多。究竟是开发者对 WWDC 的期待变淡了,还是更多人开始秉持“降低预期才能获得更多惊喜”的心理?

CocoaPods 正在退场,SwiftPM 才刚到第二章 - 肘子的 Swift 周报 #135

谷歌近期宣布,从下一个 Flutter 稳定版 3.44 开始,Swift Package Manager 将在默认路径上取代 CocoaPods,成为 iOS 和 macOS 应用的默认依赖管理器。CocoaPods 的 Trunk 仓库计划于 2026 年 12 月 2 日正式进入只读状态——这个时间点我们在 2024 年的周报中就讨论过了,但当 Flutter 真正开始在默认路径上用 SPM 替换 CocoaPods 时,还是引发了社区的广泛热议。

Swift 并发正被更广泛地接纳 - 肘子的 Swift 周报 #133

从 Swift 5.5 引入符合现代编程思想的新并发模型算起,一转眼快 5 年了。从 5.5 到目前的 6.3,Swift 社区一直在采用小步迭代的方式,积极推进并发 API 的演进。但在应对过多的新关键字、复杂的隔离概念以及一些容易引发困扰的“反模式”时,这个过程对开发者来说并不算顺利。

鸿蒙呼吸动画踩了三个坑:GPU降级时机、设计Token校验、i18n漏key——具体怎么处理的

在鸿蒙上做呼吸动画,我以为最难的是 ArkTS 语法,结果最麻烦的是——我根本不知道用户的设备跑到哪一档了。

呼吸动画是「呼吸视界」这个 App 的核心体验:吸气时圆圈缓慢扩张,屏气时保持,呼气时收缩。这个动画一旦卡顿,「跟着 App 呼吸」的节奏就断了,用户能感觉到「哪里不对」,但不会告诉你是帧率问题。

先说一下这个 App 是干什么的,方便后面的技术背景理解。

产品背景:一个给自己做的呼吸训练工具

「呼吸视界」(iOS App Store ID: 6758613852)做的是结构化呼吸训练引导——4-7-8 呼吸法、盒式呼吸、Wim Hof 法这些。网上这些方法的文字说明很多,但照着文字练,你得自己数秒、记顺序,练着练着就分心了。

我做这个的起因很功利:开会前容易紧张,想找个东西帮我两分钟之内把状态重置一下。找了一圈没找到合适的,就自己写了。

App 有三块核心功能:带动画节奏的引导式练习、本地持久化的训练记录、以及一个课程进度系统(不只是单次练习,而是完整的训练计划)。iOS 版目前评分 5 分,样本量不大,但有个用户说「可以跟随练习呼吸,保持稳定的心情」——说实话这个反馈比我预期的更朴实,我自己用下来觉得更直接的感受是:开会前真的有用,两分钟够了。

鸿蒙版最近发布,把移植过程里踩的几个坑整理一下。

坑一:呼吸动画的 GPU 降级,我不知道该在哪个阈值切

呼吸动画用 GPU 渲染时效果最好,过渡顺滑,缩放曲线自然。但鸿蒙设备碎片化比 iOS 严重得多,中低端机上 GPU 渲染直接掉帧,整个动画变得一顿一顿的。

所以我做了一套自适应降级:检测到性能不足时切到 Canvas fallback 模式,同时把当前渲染质量分成 highbalancedlow 三档。

问题来了:切换阈值怎么定?

我的判断方式是盯 frameMs(单帧渲染耗时)和连续低帧计数:

// 连续低帧超过阈值时触发降级
if (frameMs > 22 && consecutiveLowFpsCount >= 3) {
  // 22ms ≈ 45fps,低于此值且连续3帧 → 切 balanced
  adaptRenderer('degrade quality -> balanced');
  consecutiveLowFpsCount = 0;
}
if (frameMs > 33 && consecutiveLowFpsCount >= 3) {
  // 33ms ≈ 30fps,连续3帧 → 切 canvas fallback
  adaptRenderer('switch renderer -> canvas fallback');
}

这个阈值不是凭感觉拍的,是我把 hilog 日志抓出来跑脚本分析的结果。日志里会输出每帧的 fpsframeMstickMs 以及当前渲染质量档位,降级事件会打 BF_PERF_ADAPT 标签,比如 degrade quality -> balanced 或者 switch renderer -> canvas fallback

对独立开发者来说这套日志分析挺重要——没有 QA、没有用户主动反馈卡顿,只能靠工具自己发现问题。我在没有真机的情况下,靠日志回放重现了好几个卡顿场景。

目前 Canvas 模式下动画过渡还是不如 GPU 顺滑,这个还在打磨,算是没解决干净的问题。

坑二:设计 Token 漏用——一个脚本比 code review 更可靠

App 的调性是「平静克制」,UI 上我比较在意所有间距、圆角、阴影、动画时长要统一。如果哪个地方直接写了魔法数字,整体质感就散了。

鸿蒙版我把所有设计 token 收进一个 Style.ets,导出四个命名空间:SPACERADIUSSHADOWMOTION。问题是开发过程中很容易手滑——改某个组件时直接写 borderRadius(8) 而不是 RADIUS.card,这种事我自己也干过。

所以我写了一个 check_design_foundation.py,逻辑很简单:用 path.read_text() 读取关键文件内容,用字符串匹配检查是否包含预期的 token 引用。比如检查 AppBackdrop.ets 里有没有 export struct AppBackdrop,检查 SheetBackground.ets 里有没有调用 AppBackdrop(,检查根页面 RootPage.ets 里有没有 AppBackdrop({

不是正则匹配魔法数字(那个误报太多),而是检查「关键结构是否存在」——更像一个架构约束验证器。

真实案例:有一次我重构了背景组件 AppBackdrop,改了对外接口,但忘了更新 SheetBackground 里的调用方式,就是被这个脚本拦下来的。如果没有这个检查,这个问题可能得等到真机运行时才会发现。

我还把几个类似的脚本整合进一个 check_foundation_alignment.py,统一管理:设计 token 校验、按压反馈检查、页面过渡检查、i18n 对等检查、路由检查——提交前一起跑,哪个挂了去修哪个。独立开发没有 code review,这套东西算是自己给自己兜底。

坑三:i18n 漏 key,双语维护是个持续性的低级错误

App 支持中英双语,维护四个语言文件:strings_app_en.etsstrings_app_zh-Hans.ets 以及对应的 base 版本。每次加新功能往里填 key,英文填了忘了填中文,或者反过来,这种事经常发生。

check_i18n_keys.py 做的事很直白:把四个文件里的 key 全部提取出来做集合差运算,输出「哪些 key 在英文有但中文没有」以及反向的情况。

这个脚本帮我发现过好几次漏掉的 key,有时候漏的是边缘功能的文案,有时候是一个按钮标题——后者如果漏了,用户看到的就是 key 字符串本身,很难看。

课程进度系统:本地存储是主动选择,不是偷懒

ProgramProgressRecord 记录用户在某个训练计划里完成了哪些 session、当前在第几阶段。数据全部本地存储,没有云同步。

说实话云同步我也不想做。OAuth 接入、服务器费用、隐私合规、多端数据冲突处理……这一套对独立开发者来说投入产出比太低。用户的训练记录放本地就够了,鸿蒙的 Preferences 和 RelationalStore 用起来比我预期顺手,持久化这块没遇到太大麻烦。

自定义呼吸节奏的交互,我还没想明白

用户可以自由设置吸气、屏气、呼气各阶段的时长。这个功能的交互我试了三版:滑动条、数字步进器、转盘——感觉都差点意思。滑动条精度不够,步进器操作次数太多,转盘在小屏上很难操作。

这个目前还搁着,UI 做得比较简陋。如果你做过类似的时长输入控件——尤其是整数秒精度、范围大概 1-30 秒的场景——很想听听你用了什么方案。

从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI - 肘子的 Swift 周报 #132

从 2019 年问世算起,SwiftUI 已经快七年了。它早已脱去了最初几年的稚气,逐渐成为苹果生态开发者的基础能力之一。不过,SwiftUI 的闭源属性也意味着,它的很多运行机制始终不透明。开发者在使用时固然能感受到它的表达优势,但一旦遇到问题,往往很难进一步追踪原因。这种特性也让 SwiftUI 在 AI 辅助编程时代显得有些“吃亏”——相比那些长期暴露在社区讨论、源码和文档中的技术,大模型能参考的高质量材料终究有限。

被 Vibe 摧毁的版权壁垒,与开发者的新护城河 - 肘子的 Swift 周报 #131

Anthropic 不久前宣布,由于其最新模型 Mythos 在网络安全与代码漏洞挖掘方面的能力“过于强大”,已达到令人不安的程度,因此采取了极为罕见的克制措施:仅向 Project Glasswing 内的少数关键基础设施企业开放,不面向公众发布,普通开发者也无法通过 API 调用

苹果的罕见妥协:当高危漏洞遇上“拒升”潮 - 肘子的 Swift 周报 #130

对于 iOS 用户来说,最近或多或少都会看到与 Coruna、DarkSword 有关的高危漏洞消息。两个攻击链均采用水坑攻击的方式,攻击者无需受害者进行任何交互,仅需访问一个被植入恶意 iframe 的合法网站或加载恶意广告,即可触发完整的攻击链,在窃取资料后自动清理攻击痕迹。由于工具链利用的漏洞存在于 iOS 13 至 18.7 的绝大多数版本中,截止目前,已有上亿用户受到影响。

一墙之隔,不同的时空 - 肘子的 Swift 周报 #129

一年一度的 Let's Vision 大会在上海如期举行,今年的主题是:“Born to Create, Powered by AI”。除了与 Swift、空间计算相关的常规 Session,大会还邀请了许多开发者分享他们在工作中对 AI 的应用与理解。通过这些讲师对 AI 工作流的介绍,我也受益匪浅。原本只能容纳 300 人的 AI 主题会场,里三层外三层站满了热情高涨的观众。

50 岁的苹果和 51 岁的我 - 肘子的 Swift 周报 #127

再有不到半个月,Apple 将迎来 50 岁生日。Tim Cook 也发表了一篇短文,致敬过去半个世纪的历程。不过,由于苹果一直以来始终引领潮流的形象,很多人并没有意识到它已经是 IT 产业中名副其实的元老。与它年龄相当的 IT 巨头,如今仍留在一线牌桌上的寥寥无几。

运动的科学原理与健康价值 - 读《锻炼》

最近读完近期研读了哈佛大学进化生物学教授丹尼尔·利伯曼的著作《锻炼》,该书从进化生物学的视角,系统阐述了人类运动的本质及其对现代健康的重要性。本文将对书中核心观点进行梳理与总结。

锻炼是 “反人性” 的

利伯曼教授在书中开篇即指出,从进化角度看,锻炼在某种程度上是“反人性”的。人类基因在漫长的演化过程中,倾向于节约能量以应对生存挑战,如应对饥荒或繁殖需求,而非主动追求高强度体力活动。

然而,随着现代社会工具的普及,体力劳动显著减少,而人类的生理机制尚未完全适应这种快速变化的环境。因此,为了弥补体力活动不足带来的健康赤字,有意识的“锻炼”成为现代人维持健康的必要手段。值得注意的是,作者强调锻炼与娱乐性体育活动并非等同概念。

所以,我们需要接纳现在的自己,并意识到锻炼是反人性的。

静态下的身体

长期处于静态或低活动状态,可能引发慢性炎症反应,其机制主要包括:

    1. 脂肪细胞肥大: 当人体脂肪堆积过多时,脂肪细胞体积增大,可诱导白细胞聚集并释放炎症因子,进而引发慢性炎症。
    1. 久坐与代谢功能: 长时间久坐会降低身体从血液中吸收葡萄糖和脂肪的能力,这是导致全身性慢性轻度炎症的另一重要因素。
    1. 心理压力: 持续的心理压力导致皮质醇分泌增加。皮质醇不仅促使糖和脂肪进入血液循环,还可能增强对高糖高脂食物的渴望,从而促进内脏脂肪的储存。此外,过高的皮质醇水平还可能干扰睡眠周期,导致睡眠质量下降。
    1. 肌肉的抗炎作用: 肌肉不仅是运动器官,更兼具内分泌功能,能够合成并释放多种被称为“肌细胞因子”的蛋白质。这些因子具有多种生理作用,其中之一便是抑制炎症。适度的运动能够引发轻微的生理性炎症,进而刺激肌肉通过抗炎机制进行修复。

运动可以有效的抑制以上炎症反应。

人体内的能量反应

人体主要通过三磷酸腺苷(ATP)水解释放能量。ATP水解生成二磷酸腺苷(ADP)和磷酸,并释放能量和氢离子。ADP可通过“充电”过程,即利用糖分子和脂肪分子的化学反应,重新转化为ATP。

在运动过程中,能量供应遵循一定顺序:

  • ATP储备: 人体ATP储备量有限(不足100克),在运动初期迅速耗尽。
  • 磷酸原系统: 随后动用磷酸原系统,提供短暂的快速能量。
  • 糖酵解: 磷酸原耗尽后,启动糖酵解过程。此过程将一个糖分子分解为两个丙酮酸,并为两个ADP分子“充电”生成ATP。糖酵解无需氧气参与,在短时间高强度运动(如30秒冲刺)中贡献约一半的能量。然而,糖酵解会产生丙酮酸,进而分解为乳酸和氢离子。尽管乳酸本身无害,但氢离子累积会导致肌肉酸痛和疲劳,影响运动表现。
  • 有氧氧化: 在氧气充足条件下,一个糖分子通过有氧氧化产生的ATP是糖酵解的19倍。但有氧代谢过程复杂,涉及多步反应和大量酶。相比糖,脂肪燃烧产生能量所需时间更长。

在静息状态下,身体约70%的能量来源于脂肪的缓慢燃烧。然而,随着运动强度的增加,对糖的燃烧需求也随之增加。当运动强度超过有氧能力极限时,能量供应将完全依赖于糖的无氧分解。

肌肉的原理

肌肉由大量长而薄的细胞组成,称为肌纤维,每个肌纤维由数千个肌原纤维组成。再细分,肌原纤维包含数千个名为肌节的带状组织。肌节由两种重要蛋白质组成,一种细,一种粗,彼此交错,就像双手合十时手指那样。这种结构可以生成拉力,当神经向肌肉发出电信号时,就像两队拔河的人拉绳子一样,肌肉收缩的动作就发生了。

人体的肌肉纤维分为慢肌纤维和快肌纤维。

  • 慢肌纤维以有氧方式利用能量,不易产生疲劳,由于颜色暗淡,它又被称为红肌纤维。
  • 快肌纤维又分作白肌纤维和粉肌纤维。白肌纤维燃烧糖生成强烈而快速的力量,但是会很快疲劳。粉肌纤维以有氧的方式生成中等强度的力量,所以也不会很快产生疲劳。

人体很多肌肉的快肌纤维与慢肌纤维的比例大约都是 1:1。但是对于三头肌等用来发力的肌肉,快肌纤维比例就会达到 70%,而对于那些用来走路的肌肉,比如小腿的肌肉,慢肌纤维的比例就会到达 85%。

心脏健康与心血管疾病

多数心脏相关疾病源于心脏自身病变或血管问题。

动脉粥样硬化是动脉硬化的起始阶段,表现为动脉壁内斑块积聚。这些斑块由脂肪、胆固醇和钙等物质混合而成。为应对斑块对动脉壁的刺激和损伤,白细胞会启动炎症反应,将这些物质包裹并使其硬化,导致斑块逐渐增大。斑块若完全阻塞动脉或脱落后阻塞其他部位小动脉,均可导致严重后果。

高血压对心脏构成慢性损伤。长期高血压状态下,心脏为维持正常功能会增厚心肌壁,但增厚的心肌壁会逐渐硬化并被疤痕组织取代,最终导致心功能下降。

心肺训练被普遍认为是维护心血管系统的最佳运动方式。

胆固醇的生理意义

胆固醇检测通常测量血液中三种分子的水平:

  1. 低密度脂蛋白(LDL): 常被称为“坏胆固醇”。肝脏生成的气球状分子,负责在血液中运输脂肪和胆固醇。然而,某些LDL分子可能破坏并侵入动脉壁,尤其在高血压状态下,引发炎症反应并形成斑块。

  2. 高密度脂蛋白(HDL): 有时被称为“好胆固醇”。这些微小颗粒能清除LDL,并将其运回肝脏进行代谢。

  3. 甘油三酯: 自由漂浮在血液中的脂肪颗粒,是代谢综合征的重要标志物。

锻炼时长与强度建议

作者建议,成年人每周应至少进行5次,每次至少30分钟的中等强度至高强度有氧训练。

  • 中等强度训练: 心率维持在最大心率的50%~70%区间。
  • 高强度训练: 心率维持在最大心率的70%~85%区间。

最大心率的估算方法通常为220减去年龄。根据作者研究,达到上述锻炼时长可将全因死亡率降低一半。即使进一步延长锻炼时间,全因死亡率仍会下降,但下降幅度趋缓(如下图)。

此外,作者还建议每周进行两次肌肉力量增强训练,涵盖所有大肌肉群(包括腿、臀、背、核心、肩和臂),并确保每次训练后有足够的恢复时间。每个部位重复练习8~12次,进行2到3组。

小结

《锻炼》一书深刻阐明了运动对人体健康的科学益处,尤其强调了训练强度和时长的重要性。书中提出的每周150分钟有氧训练加两次力量训练的目标,为我们提供了长期健康管理的重要指引。期望读者能从中汲取知识,并将其融入日常生活中,以期实现更健康的生活方式。

Macbook Neo:苹果重回校园的起点 - 肘子的 Swift 周报 #126

上周,苹果推出了若干新款硬件产品。与以往的发布会不同,这次发布显得异常低调。起初我只对其中新发布的显示器感兴趣,但在看到不少数码媒体对 Macbook Neo 配置的吐槽后,也不由得多留意了这款产品。相较于其“减配”的表象,我更从其精准的定价中看到了苹果重返教育市场的决心。

【Flutter×鸿蒙】通关手册(二):FVM 不认鸿蒙 SDK?4步手动塞进去

系列导航:

我第一次让 FVM 管理鸿蒙版 Flutter SDK 时,前后踩了 4 个坑,花了大半天才跑通。事后复盘发现,每个坑都不难,只是没人提前告诉我"为什么要这样做"。这篇把整个过程拆成 5 关,每关讲清「为什么」和「怎么做」,争取让你 20 分钟一次通关。

前置条件:请先完成第一篇的全部内容——DevEco Studio 已安装,ohpm、node、hvigorw 在终端里都能正常调用。


🗺️ 通关路线图

关卡 任务 预计耗时
第1关 安装 FVM 2 min
第2关 克隆鸿蒙版 SDK 5 min(取决于网速)
第3关 修复版本"身份证" 3 min
第4关 指定鸿蒙 SDK 路径 1 min
第5关 全绿验证 2 min

🎯 第 1 关:安装 FVM

目标

让终端认识 fvm 命令。

为什么需要 FVM

一句话——让不同项目用不同版本的 Flutter,互不干扰。比如项目 A 用官方 3.24 跑 Android/iOS,项目 B 用鸿蒙版 3.35.8。FVM 就是 Flutter 的"版本档案柜",每个抽屉放一个版本。

📋 操作

# macOS(在终端里执行,这是用 Homebrew 包管理器安装 FVM)
brew install fvm
# Windows(在 cmd 或 PowerShell 中执行,这是用 Chocolatey 包管理器安装 FVM)
choco install fvm

安装完后,配置 FVM 缓存路径。把以下两行写入 ~/.zshrc(上一篇介绍过,这是 Mac 终端的配置文件):

# FVM 存放所有 Flutter 版本的目录
export FVM_CACHE_PATH=$HOME/fvm
# 让 FVM 的默认版本可以直接用 flutter 命令调用
export PATH="$HOME/fvm/default/bin:$PATH"

保存后执行下面这条命令,让刚才的配置立即生效(否则要关掉终端重新打开):

source ~/.zshrc

✅ 验证

# 查看 FVM 版本号,确认安装成功
fvm --version

看到版本号(如 3.1.4)就过关了。

⚠️ 如果报 command not found:Mac 用户确认已安装 Homebrew(执行 brew --version 看有没有输出);Windows 用户确认已安装 Chocolatey(执行 choco --version)。如果包管理器本身都没装,请先去官网安装。


🎯 第 2 关:克隆鸿蒙版 SDK

目标

把华为的鸿蒙版 Flutter 放进 FVM 管辖。

为什么不能直接 fvm install

正常装 Flutter 只需要 fvm install 3.24.0,FVM 会自动去 GitHub 下载。但鸿蒙版是华为团队在 AtomGit(国内代码托管平台)上单独维护的,FVM 的世界里它根本不存在。所以我们要"手动入库"——自己下载代码,放到 FVM 的档案柜里,假装它一直在那。

⚠️ 本关最大的坑:分支名和版本号是两回事!

仓库的分支叫 oh-3.35.7-dev,看到 3.35.7 你会以为版本就是 3.35.7。但实际上代码里的版本已经迭代到了 3.35.8-ohos-0.0.2

类比:Git 分支叫 feature/login-v1,但代码早就改到 v3 了。分支名是创建时起的,不会跟着版本号自动更新。

千万别拿分支名当版本号用,团队必须统一用 3.35.8-ohos-0.0.2 这个真实版本号。

📋 操作

# --depth 1 只取最新代码,省空间(省去几个 GB 的历史记录)
git clone -b oh-3.35.7-dev --depth 1 
https://atomgit.com/openharmony-tpc/flutter_flutter.git 
~/fvm/versions/3.35.8-ohos-0.0.2

注意看:clone 命令里分支名是 oh-3.35.7-dev,但目标文件夹名是 3.35.8-ohos-0.0.2——这不是写错了,上面已经解释了为什么不一样。

💡 怎么确认真实版本号? clone 完后执行 ~/fvm/versions/3.35.8-ohos-0.0.2/bin/flutter --version 看输出。如果加入已有团队,直接看项目的 .fvmrc 文件(命令:cat .fvmrc)。

✅ 验证

# 确认文件下载成功(ls = 列出目录内容)
ls ~/fvm/versions/3.35.8-ohos-0.0.2/bin/flutter

文件存在就过关。

⚠️ 如果报 No such file or directory:回去检查 clone 命令是否执行成功。常见原因是网络超时(AtomGit 在国内,通常不需要梯子,但公司内网可能有限制)。重新执行 clone 前,先删掉残留目录:rm -rf ~/fvm/versions/3.35.8-ohos-0.0.2,再重试。


🎯 第 3 关:修复版本"身份证"

目标

让 FVM 正确识别这个 SDK 的版本号。

为什么要做这步

clone 下来的 SDK 有两张"证件":

  1. version 文件——相当于身份证,一行文本写着版本号
  2. bin/cache/flutter.version.json——相当于内部档案,JSON 格式的详细版本信息

问题是,这两张证件上都写着 0.0.0-unknown(因为鸿蒙团队是从开发分支构建的,没有打标准标签)。但我们的文件夹名叫 3.35.8-ohos-0.0.2。FVM 一查——名字对不上,直接翻脸。

⚠️ 不做这步的后果:FVM 会弹出 "Version mismatch" 并试图删掉你的 SDK 重装。如果看到了这个弹窗,千万不要选任何选项,按 Ctrl+C(Mac 也是 Ctrl 不是 Cmd)退出,回来做这步。

📋 操作

macOS / Linux:

cd ~/fvm/versions/3.35.8-ohos-0.0.2

# 第一步:改"身份证"
echo -n "3.35.8-ohos-0.0.2" > version

# 第二步:初始化 Flutter 引擎(首次运行会下载 Dart SDK,需要等 1-3 分钟)
bin/flutter --version

# 第三步:改"内部档案"(把所有 0.0.0-unknown 替换成正确的版本号)
sed -i '' 's/0.0.0-unknown/3.35.8-ohos-0.0.2/g' bin/cache/flutter.version.json

Windows PowerShell:

# 进入 SDK 所在目录
cd $env:USERPROFILE\fvm\versions\3.35.8-ohos-0.0.2

# 第一步:改"身份证"
"3.35.8-ohos-0.0.2" | Set-Content version -NoNewline

# 第二步:初始化引擎
bin\flutter --version

# 第三步:改"内部档案"(PowerShell 的查找替换写法)
(Get-Content bin\cache\flutter.version.json) -replace '0.0.0-unknown', '3.35.8-ohos-0.0.2' | Set-Content bin\cache\flutter.version.json

⚠️ 三步的顺序不能乱——第二步会生成 flutter.version.json 文件,第三步才有东西可改。如果你先执行了第三步,会报文件不存在。

✅ 验证

# 回到任意目录都可以执行(fvm list = 列出 FVM 管理的所有 Flutter 版本)
fvm list

看到 Version 列显示 3.35.8-ohos-0.0.2(不是空白、不是 Need setup、不是 0.0.0-unknown),这关就过了。

02_fvm_list.png ⚠️ 如果还是显示异常,逐一排查两张"证件":

# 检查"身份证"内容
cat ~/fvm/versions/3.35.8-ohos-0.0.2/version
# 应该输出:3.35.8-ohos-0.0.2(没有多余空行)

# 检查"内部档案"有没有残留的 0.0.0-unknown
cat ~/fvm/versions/3.35.8-ohos-0.0.2/bin/cache/flutter.version.json
# 里面所有 version 字段应该都是 3.35.8-ohos-0.0.2

如果 version 文件内容不对,重新执行第一步;如果 JSON 里还有 0.0.0-unknown,重新执行第三步。


🎯 第 4 关:指定鸿蒙 SDK 路径

目标

让 Flutter 知道鸿蒙的 SDK(OpenHarmony SDK)装在哪。

为什么不用环境变量

我试过 HOS_SDK_HOMEOHOS_SDK_HOME 等环境变量,时灵时不灵。原因是不同方式打开的终端(VS Code 内置终端 vs 系统终端 vs CI 环境)加载配置文件的顺序不一样,变量可能没被读到。flutter config 会把路径写入 Flutter 自己的配置文件,不管从哪里启动都能读到,最稳。

📋 操作

# 把鸿蒙 SDK 的位置"写死"到 Flutter 的配置里(一次性操作)
~/fvm/versions/3.35.8-ohos-0.0.2/bin/flutter config \
--ohos-sdk="/Applications/DevEco-Studio.app/Contents/sdk"

⚠️ Windows 用户路径改为:--ohos-sdk="C:\Program Files\Huawei\DevEco Studio\sdk"

请根据 DevEco Studio 实际安装路径调整。不确定装在哪?打开 DevEco Studio → Settings → SDK 页面可以看到路径。

终端输出 Setting "ohos-sdk" value to "..." 就成功了。

✅ 验证

不急,下一关一起验收。


🎯 第 5 关:全绿验证

目标

flutter doctor 中 HarmonyOS toolchain 一栏显示绿色对勾。

📋 操作

# 运行 Flutter 的环境诊断工具(-v 表示显示详细信息)
~/fvm/versions/3.35.8-ohos-0.0.2/bin/flutter doctor -v

✅ 验证

关注输出中的 HarmonyOS 那一栏:

[✓] HarmonyOS toolchain - develop for HarmonyOS devices
    • OpenHarmony Sdk at /Applications/DevEco-Studio.app/Contents/sdk,
      available api versions has [22:default]
    • Ohpm version 6.0.1
    • Node version v18.20.1
    • Hvigorw binary at .../hvigor/bin/hvigorw

看到 [✓] 加上 4 个子项都有值 = 通关!

02_flutter_doctor.png 💡 你可能会看到 Flutter 那栏有几个 ! 警告(channel 不标准、upstream 不是官方地址)。这是鸿蒙版的正常现象,完全不影响开发和打包,放心忽略。

⚠️ 如果 HarmonyOS 那栏还是红叉,按优先级排查:

  1. SDK not found → 回第 4 关检查 config 路径是否正确
  2. ohpm/hvigorw missing → 回第一篇检查环境变量
  3. Version mismatch → 回第 3 关检查两张"证件"

🔧 附加关:FVM 的"碎碎念"

通关后你会发现,每次用 fvm flutter xxx 时 FVM 都会弹 "not a valid version" 的警告让你确认。这不是报错,只是 FVM 在说:"这个版本号我在官方列表里查不到,你确定要用吗?"

三种应对方式:

  1. 手动按 y——每次弹出输入 y 回车
  2. 自动确认——命令前加 yes |
yes | fvm flutter doctor
  1. 绕过 FVM——直接用绝对路径调用,完全不弹警告:
~/fvm/versions/3.35.8-ohos-0.0.2/bin/flutter doctor

我推荐第三种,路径虽长但最省心。可以设个快捷方式(alias)缩短它:

# 把这行加到 ~/.zshrc 里(alias = 给一条长命令起个短名字)
alias hflutter="$HOME/fvm/versions/3.35.8-ohos-0.0.2/bin/flutter"

保存后 source ~/.zshrc,之后直接 hflutter runhflutter doctor 就行。


🏆 通关总结

项目 状态
FVM ✅ 已安装
鸿蒙版 Flutter SDK ✅ ~/fvm/versions/3.35.8-ohos-0.0.2
version 文件 ✅ 已修复
flutter.version.json ✅ 已修复
flutter config --ohos-sdk ✅ 已配置
flutter doctor HarmonyOS ✅ 全绿

回顾核心逻辑:FVM 只管官方 Flutter,鸿蒙版要我们手动塞进去(第 2 关);塞进去后"证件"信息对不上,需要手动修正(第 3 关);最后告诉 Flutter 鸿蒙 SDK 在哪(第 4 关)。理解了这条线,以后鸿蒙版 SDK 升级换版本号,你也能照样搞定。

如果中途卡住,大概率是版本号写错了——检查文件夹名、version 文件内容、flutter.version.json 里的版本号,三者必须完全一致


下一篇预告:SDK 准备好了,接下来要把你的老 Flutter 项目跑到鸿蒙上——听起来就是敲几行命令的事?没那么简单。→ 【Flutter×鸿蒙】通关手册(三):debug 包也要签名,这点和 Android 差远了

跨域传递 NSManagedObjectContext 为什么在 Swift 6.2 中不再报错?真正的变化不在编译器

当同一段与并发有关的代码在 Xcode 16 中无法通过,却能在 Xcode 26 中顺利编译时,你第一时间会想到什么?我最初的判断是编译器进化了,但现实并没有这么简单。本文将记录我最近遇到的一次有意思的排查过程:从测试失败出发,一步步追到 Core Data 的 SDK interface,最终发现,问题的关键并不完全在 Swift 编译器本身,而在 NSManagedObjectContext 被导入 Swift 的方式已经发生了变化。

❌