一场关于 SwiftUI 动画的“原生”之争 -- 肘子的 Swift 周报 #154
上周,React Native 核心开发者 Krzysztof Magiera 的一条 推文 在苹果开发生态中引发热烈讨论。推文中他展示了一个示例:在主线程阻塞的情况下,UIKit 的一个往返动画仍能正常执行,而 SwiftUI 的动画则停滞了。他认为原因在于 SwiftUI 没有使用 Core Animation 的 render-server animation,而是在应用进程中推进动画,因此对 SwiftUI 是否足够“原生”提出了质疑;随后又表示,在 React Native 中,开发者仍可通过 Core Animation 创建不受主线程阻塞影响的动画。
SwiftUI 共同创造者、早期核心开发者 Kyle Macomber 对此回应了不同的 观点。他解释说,SwiftUI 在设计之初就有意放弃了 Server-side Animation(由 Core Animation 在 render server 中推进动画),而选择让动画值在 client / app process 中计算和推进,以换取更好的可中断性与交互性。这并非技术上的退步,而是一项设计取舍(SwiftUI 并没有放弃 Core Animation,它仍然参与后续的渲染过程,改变的只是动画由谁来推进)。
动画的易用性一直是 SwiftUI 的标志性特色之一,近年来不断完善的弹簧动画,又进一步提升了动画衔接的自然程度和交互表现力。作为一个响应式框架,把动画推进放在 client 端,我认为完全可以理解。当然,代价也很明确:正如 Krzysztof 的示例所示,当动画推进依赖应用进程时,它更容易受到主线程繁忙的影响,也会占用更多 client 端的计算资源。
不过,client-side animation 并不意味着所有插值计算都必须发生在主线程。随着 SwiftUI 不断演进,部分动画以及 Shape 等计算密集型工作已经可以移到后台线程,以减轻主线程的压力。不过,由于动画的推进仍然属于应用进程,异步计算也受到状态更新和渲染流程等条件的限制,因此当主线程被完全阻塞时,动画依然可能停滞。尽管存在这种“弊端”,但从 iOS 18 开始,UIKit 和 AppKit 也开始采纳这套 client-side animation 模型:新的动画 API 可以直接使用 SwiftUI 的 Animation,并由同一套动画基础设施驱动,而不再生成传统的 CAAnimation。
本次讨论呈现出一个有趣的场景:表面上在争论 SwiftUI 是否“原生”,实际展现的却是两种框架设计理念的差异。
作为苹果面向未来的 UI 框架,SwiftUI 在设计时选择走出既有路径,放弃对 Core Animation render-server animation 模型的依赖,以换取更灵活、更强大的动画表达能力。事实上,Core Animation 的创造者 John Harper 本人也是 SwiftUI 的共同创造者之一,这项选择显然不是因为不了解 Core Animation,而是在理解其优势与局限之后做出的架构决定。这还带来了一个隐形优势:SwiftUI 的部分核心能力与苹果平台的具体实现得以进一步解耦。尽管苹果并未将 SwiftUI 开源,但从架构上看,它的一些核心思想并不天然局限于苹果平台。
而从 React Native 的角度看,Krzysztof 的表述也有其合理性。作为一个跨平台框架,React Native 的重要目标之一,就是在不同平台上尽可能利用平台已经提供的能力,让应用在行为和性能上更像“本地人”。
一个最初围绕“什么才算原生”的讨论,最终引出了两边核心开发者对 UI 动画架构的理解与阐述。作为一个吃瓜群众,我也从中收获不少。
原创
最近一直在等待 Xcode 27.1 的到来,以便在 iPhone Duo 的模拟器中测试一下应用的适配情况。有意思的是,27.1 没等到,苹果率先发布了 27.2 beta。尽管 iPhone Duo 模拟器依然缺席,但新增的 project.xcproj 令人眼前一亮。它替代的是 .xcodeproj 包里的那份构建图 project.pbxproj,而不是整个工程包。它有哪些亮点,解决了什么问题,又有哪些事情没有改变,本文将对此进行探讨。
近期推荐
当 AI Agent 在本地运行时,权限早已授予,工具链已经就绪,遇到弹窗也可以人工干预。搬到云端后,这些理所当然的条件都需要重新构建。换来的好处并不在于能做到本地做不到的事,而是让开发环境本身成为可以分配给 Agent 的资源:任务可以异步运行、彼此隔离,闲置时暂停并保存状态,需要时再恢复,也更容易支持多个 Agent 并行工作。Jon Kelley 在文中介绍了为 Devin 构建 macOS 云端环境的过程,涵盖基于 Virtualization.framework 的虚拟机、磁盘快照、网络隔离、环境与权限预配置、桌面与 Simulator 的实时串流,以及借助无障碍树实现的计算机操作。最后一点对苹果开发者尤其有启发:做好无障碍支持,也是在为 Agent 的自动化验证提供结构化接口。
传统的崩溃处理有一个棘手的问题:当 signal handler 开始工作时,它所在的进程本身可能已经处于不可靠状态,内存可能损坏、锁可能没有释放,很多平时使用的 API 也无法安全调用。iOS 27 新增的 CrashReportExtension 改变了这一执行模型。当 App 崩溃后,系统会启动一个独立的 Extension,并通过只读的 CrashedProcess 让它检查已经崩溃的进程。
Anton Gubarenko 在文中完整演示了这个扩展的创建与使用,也给出了几条务实的判断。该 API 是对系统 .ips 报告的补充而非替代,适合需要自有后端、业务上下文关联或设备端预分析的团队。扩展本身应尽量精简,只负责生成并持久化报告,上传、重试等工作交给主应用,两者通过 App Group 共享存储。对多数独立开发者而言,系统崩溃报告加上 MetricKit 依然足够。
对于动画,开发者只需改变状态并设置动画函数,剩下的工作便交给 SwiftUI。但这些声明最终是如何转化成屏幕上一帧帧连续变化的画面?Kyle Ye 从 Transaction 和 AttributeGraph 出发,结合 OpenSwiftUI 的开发经验,逐步分析 SwiftUI 如何创建动画上下文、计算中间状态,并在动画过程中持续更新视图与布局。
与将动画插值主要交给 render server 的传统方式不同,SwiftUI 的许多动画需要在应用进程中持续推进。由于插值发生在视图图内部,任何满足 Animatable 的值都能被采样,布局、Shape 等更多状态因此得以参与动画,而 render server 主要处理图层属性。文中对异步提交路径中 CABasicAnimation 的分析尤其值得一读:它只承载当前帧的采样结果,下一帧便会被替换——屏幕上出现了 CA 动画对象,并不意味着动画的时钟已经交给了 Core Animation。这也为本期周评中提到的 SwiftUI 动画是否为“原生”之争提供了更多细节。
iPhone Duo 的多种使用形态给 Toolbar 带来了新的挑战。为了充分利用更宽的屏幕并保留纵向内容空间,系统会在部分形态下将原本位于顶部和底部的控件移到屏幕侧边。但传统 Toolbar 中的文本按钮、自定义视图等内容并不一定适合纵向排列,Toolbar 与 Tab Bar 还可能需要争夺有限的侧边空间。iOS 27.1 为此增加了 ToolbarItemAxisBehavior、ToolbarVerticalBehavior、ToolbarVerticalCompressionBehavior 和 toolbarVerticalEdge 等 API,让开发者可以进一步控制 Toolbar 的方向、位置以及空间不足时的压缩策略。Itsuki 通过代码和演示展示了这些新 API 的具体行为。
这批 API 的共同特征,是让开发者声明偏好而非指定结果。面对 Duo 带来的多种形态,与其针对特定设备状态编写布局,不如描述内容之间的关系和布局偏好,再由 SwiftUI 根据实际环境决定最终呈现方式。
当 iPhone Duo 处于部分折叠状态时,中央折痕形成了一块不适合交互的区域,按钮等控件如果恰好落在这里,会变得难以操作。Sarun Wongpatcharapakorn 以 Sheet 为例,展示了同一个界面如何在 Duo 的关闭、展开、横屏、竖屏以及部分折叠等不同形态下调整尺寸、位置和 Toolbar。Sheet、Alert 和菜单等系统组件已经内置折痕规避能力,无需开发者干预。因此对于开发者而言,优先使用系统组件,也意味着可以直接获得这些针对不同设备形态的自适应行为。
工具
在 Vapor 诞生十周年、距离 Vapor 4 发布六年半之际,社区终于迎来了 Vapor 5 的首个 Beta 版本。此次几乎是一次彻底重写——历时两年、变更约 49,000 行代码。最核心的改动是彻底告别 EventLoop 与 EventLoopFuture,全面转向 Swift Concurrency,并以结构化并发为基础,NIO 也基本从公开 API 中隐去。
同时,Vapor 5 进一步整合 Swift HTTP Types、Swift Configuration 和 Swift Service Lifecycle,并加入实验性的 VaporMacros,为路由和认证提供更多编译期类型检查。Vapor 5 要求 Swift 6.4,目前仍处于 Beta 阶段。有关更多变化,可以查看 Tim Condon 的 What’s New in Vapor 5 Beta。
Xcode 27.1 的 Device Hub 可以调整 iPhone Duo Simulator 的折叠角度(按住 Option 键可见),但 simctl 和 devicectl 并没有提供对应的控制命令。Artem Novichkov 开发的 Hinge 补上了这个缺口,可以通过命令行设置 0–180° 的折叠角度,也支持打开、关闭、半折叠以及连续改变角度等操作。Hinge 还提供了 Agent Skill,让 Coding Agent 可以在测试过程中主动改变 Duo 的折叠状态并验证 App。
在移动端实现 Markdown,常见方案是套一层 WebView。Software Mansion 推出的 Enriched Markdown 选择了一条更彻底的路线:基于各平台的原生文本能力完成 Markdown 渲染与富文本编辑,并可以将编辑后的内容重新输出为干净的 Markdown。
它提供独立的 SwiftUI SDK,基于 TextKit 2 与 md4c 实现 Markdown 渲染,同时也覆盖 React Native、Android 和 Web。相比单纯追求“能显示 Markdown”,Enriched Markdown 更重视原生体验:文本可以正常选择,链接、图片与代码块能够接入系统菜单,并适配 VoiceOver、TalkBack、动态字体和从右到左书写(RTL)。