Fatbobman's Swift Weekly #154
A “Native” Debate Over SwiftUI Animation
Last week, a post by React Native core developer Krzysztof Magiera sparked a lively discussion across the Apple developer community. In the post, he demonstrated an example where, with the main thread blocked, a back-and-forth animation in UIKit continued to run normally while the equivalent SwiftUI animation came to a halt. He attributed this to SwiftUI not using Core Animation’s render-server animation model and instead advancing animations within the app process, raising the question of whether SwiftUI is truly “native.” He later added that, in React Native, developers can still use Core Animation to create animations that continue running even when the main thread is blocked.
SwiftUI co-creator and early core developer Kyle Macomber responded with a different perspective. He explained that, from the beginning, SwiftUI deliberately moved away from server-side animation—where Core Animation advances animations on the render server—and instead chose to compute and advance animation values in the client/app process in exchange for better interruptibility and interactivity. This was not a technical regression, but a deliberate design trade-off. (SwiftUI did not abandon Core Animation itself; Core Animation still participates in the subsequent rendering process. What changed is who advances the animation.)
Ease of animation has always been one of SwiftUI’s signature strengths, and the continued evolution of its spring animations in recent years has further improved the naturalness of transitions and the expressiveness of interactive motion. For a reactive framework, I think advancing animations on the client side is entirely understandable. The trade-off, of course, is equally clear: as Krzysztof’s example demonstrates, when animation advancement depends on the app process, it becomes more susceptible to a busy main thread and also consumes more computational resources on the client side.
That said, client-side animation does not mean that all animation sampling calculations must happen on the main thread. As SwiftUI has evolved, some computationally intensive work involving animations and Shape can now be moved to background threads, reducing pressure on the main thread. Still, because animation advancement remains within the app process, and asynchronous computation is subject to constraints such as state updates and the rendering pipeline, animations can still stall when the main thread is completely blocked. Despite this “drawback,” starting with iOS 18, UIKit and AppKit also began adopting this client-side animation model: their new animation APIs can directly use SwiftUI’s Animation and are driven by the same underlying animation infrastructure rather than generating traditional CAAnimation objects.
This discussion presented an interesting situation: on the surface, the debate was about whether SwiftUI is truly “native,” but underneath it revealed a difference in framework design philosophies.
As Apple’s UI framework for the future, SwiftUI chose to depart from the established path, moving away from dependence on Core Animation’s render-server animation model in exchange for more flexible and expressive animation capabilities. In fact, John Harper, the creator of Core Animation, was himself one of SwiftUI’s co-creators. So this choice clearly wasn’t made out of unfamiliarity with Core Animation, but rather as an architectural decision made with an understanding of both its strengths and limitations. It also brought a less obvious advantage: some of SwiftUI’s core capabilities became further decoupled from the specific implementation details of Apple platforms. Although Apple has not open-sourced SwiftUI, architecturally, some of its core ideas are not inherently limited to Apple platforms.
From the React Native perspective, Krzysztof’s argument also has its rationale. As a cross-platform framework, one of React Native’s important goals is to make the best possible use of the capabilities each platform already provides, allowing apps to behave and perform more like “locals” on each platform.
What began as a discussion about “what counts as native” ultimately led core developers from both sides to explain how they think about UI animation architecture. As someone happily watching from the sidelines, I learned quite a bit from it as well.
Previous Issue|Newsletter Archive
📢 Sponsor Fatbobman’s Swift Weekly
Promote your product to Swift & iOS developers across:
- Blog: 50,000+ monthly visitors
- Newsletter: 5,000+ subscribers, 50% open rate
Perfect for developer tools, courses, and services.
Enjoyed this issue? Buy me a coffee ☕️
Original
From pbxproj to xcproj: Xcode Project Configuration Gets a JSON Format
I’ve been waiting for Xcode 27.1, hoping to test how my apps adapt in the iPhone Duo simulator. Amusingly, 27.1 never arrived — Apple shipped 27.2 beta first instead. The iPhone Duo simulator is still missing, but the new project.xcproj caught my eye. What it replaces is project.pbxproj, the build graph inside the .xcodeproj bundle, not the bundle itself. What’s good about it, what does it fix, and what stays the same? This article takes a look.
Recent Recommendations
How we built Devin’s Mac
When an AI agent runs locally, permissions have long since been granted, the toolchain is ready, and a human can dismiss any dialog that appears. Move to the cloud and every one of those assumptions has to be rebuilt. The payoff isn’t doing something the local machine can’t — it’s turning the development environment itself into a resource you can assign to an agent: tasks run asynchronously, stay isolated from one another, pause and preserve their state when idle, resume on demand, and scale more easily to several agents working in parallel. Jon Kelley walks through building Devin’s macOS cloud environment, covering virtual machines on Virtualization.framework, disk snapshots, network isolation, pre-configured environments and permissions, live streaming of the desktop and Simulator, and computer use driven by the accessibility tree. That last point is especially instructive for Apple developers: good accessibility support also gives agents a structured interface for automated verification.
iOS 27: CrashReportExtension Framework
Traditional crash handling has an awkward problem: by the time the signal handler runs, the process it lives in may already be unreliable — memory may be corrupted, locks may still be held, and many everyday APIs can’t be called safely. The new CrashReportExtension in iOS 27 changes that execution model. After the app crashes, the system launches a separate extension and hands it a read-only CrashedProcess for inspecting the crashed process.
Anton Gubarenko demonstrates how to create and use the extension, along with several practical judgments. The API complements Apple’s .ips reports rather than replacing them, and suits teams that need their own backend, correlation with business context, or on-device pre-analysis. The extension itself should stay small, doing nothing more than generating and persisting a report; uploads and retries belong in the host app, with the two sharing storage through an App Group. For most indie developers, system crash reports plus MetricKit remain sufficient.
How SwiftUI animation works
For animation, developers only change state and specify an animation function, leaving the rest to SwiftUI. But how do those declarations become a continuous sequence of frames on screen? Starting from Transaction and AttributeGraph, and drawing on his work on OpenSwiftUI, Kyle Ye analyzes step by step how SwiftUI creates an animation context, computes intermediate states, and keeps updating views and layout as the animation runs.
Unlike the conventional approach of leaving interpolation mostly to the render server, many SwiftUI animations advance within the app process. Because interpolation happens inside the view graph, any value conforming to Animatable can be sampled, which lets layout, Shape, and much more participate in animation, while the render server mainly handles layer properties. The analysis of the CABasicAnimation on the asynchronous commit path is particularly worth reading: it carries only the current frame’s sampled value and is replaced on the next frame — the presence of a CA animation object on screen does not mean the animation’s clock has been handed to Core Animation. This adds useful detail to the debate over whether SwiftUI animation is “native,” discussed in this issue’s editorial.
SwiftUI: Some New APIs/ContainerViews Useful For Duo!
The many poses of iPhone Duo present new challenges for toolbars. To make full use of the wider screen while preserving vertical space for content, the system moves controls normally placed at the top and bottom to the side of the screen in certain poses. But text buttons, custom views, and other traditional toolbar content don’t always suit a vertical arrangement, and the toolbar and tab bar may have to compete for limited side space. iOS 27.1 addresses this with APIs including ToolbarItemAxisBehavior, ToolbarVerticalBehavior, ToolbarVerticalCompressionBehavior, and toolbarVerticalEdge, giving developers finer control over a toolbar’s orientation, placement, and compression strategy when space runs short. Itsuki demonstrates how these new APIs behave, with code and videos.
What these APIs have in common is that they let developers declare a preference rather than dictate a result. Faced with the range of poses Duo introduces, describing the relationships between pieces of content and your layout preferences — and letting SwiftUI decide the final presentation based on the actual environment — beats writing layouts for specific device states.
Sheets and fold avoidance on iPhone Duo
When iPhone Duo is partially folded, the crease down the middle forms a region poorly suited to interaction, and buttons or other controls that land there become awkward to operate. Using sheets as his example, Sarun Wongpatcharapakorn shows how a single interface adjusts its size, position, and toolbar across Duo’s closed, open, landscape, portrait, and partially folded poses. System components including sheets, alerts, and menus already avoid the fold without developer intervention. For developers, that means reaching for system components first also means inheriting this adaptive behavior across device poses for free.
Tools
Vapor 5 Beta Released
Ten years after Vapor was born and six and a half years after Vapor 4 shipped, the community finally has the first beta of Vapor 5. It amounts to a near-complete rewrite — two years of work and roughly 49,000 lines changed. The central change is saying goodbye to EventLoop and EventLoopFuture and moving fully to Swift Concurrency built on structured concurrency, with NIO now largely hidden from the public APIs.
Vapor 5 also integrates more deeply with Swift HTTP Types, Swift Configuration, and Swift Service Lifecycle, and adds the experimental VaporMacros for compile-time type checking of routes and authentication. It requires Swift 6.4 and remains in beta. For a fuller account of what changed, see Tim Condon‘s What’s New in Vapor 5 Beta.
Hinge: Set the Duo Simulator’s Fold Angle from the Command Line
Xcode 27.1’s Device Hub can adjust the fold angle of the iPhone Duo Simulator (hold Option to reveal it), but neither simctl nor devicectl offers a corresponding command. Hinge, from Artem Novichkov, fills the gap: it sets fold angles from 0 to 180° on the command line, and supports opening, closing, half-folding, and sweeping continuously between angles. Hinge also ships an Agent Skill, letting a coding agent change Duo’s fold state itself while verifying an app.
Enriched Markdown: Cross-Platform Markdown Rendering and Editing Without a WebView
The usual way to handle Markdown on mobile is to wrap it in a WebView. Enriched Markdown, from Software Mansion, takes a more thorough route: it renders Markdown and supports rich-text editing through each platform’s native text capabilities, and can emit clean Markdown again once the content has been edited.
It offers a standalone SwiftUI SDK, rendering Markdown with TextKit 2 and md4c, and also covers React Native, Android, and the web. Rather than settling for “Markdown that displays,” Enriched Markdown emphasizes a native feel: text selects normally, links, images, and code blocks hook into system menus, and the library supports VoiceOver, TalkBack, Dynamic Type, and right-to-left writing.
Thanks for reading Fatbobman’s Swift Weekly! This post is public so feel free to share it.
一场关于 SwiftUI 动画的“原生”之争
上周,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 动画架构的理解与阐述。作为一个吃瓜群众,我也从中收获不少。
如果您发现这份周报或我的博客对您有所帮助,可以考虑通过 Buy Me a Coffee 支持我的创作。
原创
从 pbxproj 到 xcproj:Xcode 工程配置迎来 JSON 格式
最近一直在等待 Xcode 27.1 的到来,以便在 iPhone Duo 的模拟器中测试一下应用的适配情况。有意思的是,27.1 没等到,苹果率先发布了 27.2 beta。尽管 iPhone Duo 模拟器依然缺席,但新增的 project.xcproj 令人眼前一亮。它替代的是 .xcodeproj 包里的那份构建图 project.pbxproj,而不是整个工程包。它有哪些亮点,解决了什么问题,又有哪些事情没有改变,本文将对此进行探讨。
近期推荐
为 Devin 构建云上 Mac 开发环境 (How we built Devin’s Mac)
当 AI Agent 在本地运行时,权限早已授予,工具链已经就绪,遇到弹窗也可以人工干预。搬到云端后,这些理所当然的条件都需要重新构建。换来的好处并不在于能做到本地做不到的事,而是让开发环境本身成为可以分配给 Agent 的资源:任务可以异步运行、彼此隔离,闲置时暂停并保存状态,需要时再恢复,也更容易支持多个 Agent 并行工作。Jon Kelley 在文中介绍了为 Devin 构建 macOS 云端环境的过程,涵盖基于 Virtualization.framework 的虚拟机、磁盘快照、网络隔离、环境与权限预配置、桌面与 Simulator 的实时串流,以及借助无障碍树实现的计算机操作。最后一点对苹果开发者尤其有启发:做好无障碍支持,也是在为 Agent 的自动化验证提供结构化接口。
iOS 27: CrashReportExtension Framework
传统的崩溃处理有一个棘手的问题:当 signal handler 开始工作时,它所在的进程本身可能已经处于不可靠状态,内存可能损坏、锁可能没有释放,很多平时使用的 API 也无法安全调用。iOS 27 新增的 CrashReportExtension 改变了这一执行模型。当 App 崩溃后,系统会启动一个独立的 Extension,并通过只读的 CrashedProcess 让它检查已经崩溃的进程。
Anton Gubarenko 在文中完整演示了这个扩展的创建与使用,也给出了几条务实的判断。该 API 是对系统 .ips 报告的补充而非替代,适合需要自有后端、业务上下文关联或设备端预分析的团队。扩展本身应尽量精简,只负责生成并持久化报告,上传、重试等工作交给主应用,两者通过 App Group 共享存储。对多数独立开发者而言,系统崩溃报告加上 MetricKit 依然足够。
SwiftUI 动画的运作机制 (How SwiftUI animation works)
对于动画,开发者只需改变状态并设置动画函数,剩下的工作便交给 SwiftUI。但这些声明最终是如何转化成屏幕上一帧帧连续变化的画面?Kyle Ye 从 Transaction 和 AttributeGraph 出发,结合 OpenSwiftUI 的开发经验,逐步分析 SwiftUI 如何创建动画上下文、计算中间状态,并在动画过程中持续更新视图与布局。
与将动画插值主要交给 render server 的传统方式不同,SwiftUI 的许多动画需要在应用进程中持续推进。由于插值发生在视图图内部,任何满足 Animatable 的值都能被采样,布局、Shape 等更多状态因此得以参与动画,而 render server 主要处理图层属性。文中对异步提交路径中 CABasicAnimation 的分析尤其值得一读:它只承载当前帧的采样结果,下一帧便会被替换——屏幕上出现了 CA 动画对象,并不意味着动画的时钟已经交给了 Core Animation。这也为本期周评中提到的 SwiftUI 动画是否为“原生”之争提供了更多细节。
为 iPhone Duo 准备的新 API (SwiftUI: Some New APIs/ContainerViews Useful For Duo!)
iPhone Duo 的多种使用形态给 Toolbar 带来了新的挑战。为了充分利用更宽的屏幕并保留纵向内容空间,系统会在部分形态下将原本位于顶部和底部的控件移到屏幕侧边。但传统 Toolbar 中的文本按钮、自定义视图等内容并不一定适合纵向排列,Toolbar 与 Tab Bar 还可能需要争夺有限的侧边空间。iOS 27.1 为此增加了 ToolbarItemAxisBehavior、ToolbarVerticalBehavior、ToolbarVerticalCompressionBehavior 和 toolbarVerticalEdge 等 API,让开发者可以进一步控制 Toolbar 的方向、位置以及空间不足时的压缩策略。Itsuki 通过代码和演示展示了这些新 API 的具体行为。
这批 API 的共同特征,是让开发者声明偏好而非指定结果。面对 Duo 带来的多种形态,与其针对特定设备状态编写布局,不如描述内容之间的关系和布局偏好,再由 SwiftUI 根据实际环境决定最终呈现方式。
iPhone Duo 上的 Sheet 与折痕规避 (Sheets and fold avoidance on iPhone Duo)
当 iPhone Duo 处于部分折叠状态时,中央折痕形成了一块不适合交互的区域,按钮等控件如果恰好落在这里,会变得难以操作。Sarun Wongpatcharapakorn 以 Sheet 为例,展示了同一个界面如何在 Duo 的关闭、展开、横屏、竖屏以及部分折叠等不同形态下调整尺寸、位置和 Toolbar。Sheet、Alert 和菜单等系统组件已经内置折痕规避能力,无需开发者干预。因此对于开发者而言,优先使用系统组件,也意味着可以直接获得这些针对不同设备形态的自适应行为。
工具
Vapor 5 Beta 发布
在 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。
Hinge:命令行调节 Duo 模拟器折叠角度
Xcode 27.1 的 Device Hub 可以调整 iPhone Duo Simulator 的折叠角度(按住 Option 键可见),但 simctl 和 devicectl 并没有提供对应的控制命令。Artem Novichkov 开发的 Hinge 补上了这个缺口,可以通过命令行设置 0–180° 的折叠角度,也支持打开、关闭、半折叠以及连续改变角度等操作。Hinge 还提供了 Agent Skill,让 Coding Agent 可以在测试过程中主动改变 Duo 的折叠状态并验证 App。
Enriched Markdown:不依赖 WebView 的跨平台 Markdown 渲染与编辑方案
在移动端实现 Markdown,常见方案是套一层 WebView。Software Mansion 推出的 Enriched Markdown 选择了一条更彻底的路线:基于各平台的原生文本能力完成 Markdown 渲染与富文本编辑,并可以将编辑后的内容重新输出为干净的 Markdown。
它提供独立的 SwiftUI SDK,基于 TextKit 2 与 md4c 实现 Markdown 渲染,同时也覆盖 React Native、Android 和 Web。相比单纯追求“能显示 Markdown”,Enriched Markdown 更重视原生体验:文本可以正常选择,链接、图片与代码块能够接入系统菜单,并适配 VoiceOver、TalkBack、动态字体和从右到左书写(RTL)。