无论是 SwiftUI 还是 SwiftData,这些苹果寄予厚望的基础框架,在推出时都描绘了充满光明的未来,但实际走势似乎都和最初的设定不太一样。当我越深入了解这些框架,就越对它们精妙的架构设计所折服,同时也对那些不尽如人意的实现感到无语。眼看着这些设计的光环逐渐褪去,心中不免唏嘘。
传闻苹果在今年即将发布的全新操作系统(包括 iOS 27 和 macOS 27)中,将采取类似于当年 Mac OS X Snow Leopard 时代的调整策略——将重心放在系统稳定性、性能优化、清理老旧代码和修复 Bug 上,而不是引入颠覆性的视觉设计或繁多的底层新功能。如果真能如此,那实在是令人欣慰。回看两年多前的 第六期周报,当时也传出过苹果要集中修复彼时存在的缺陷并提高软件性能,但至少从过去两年的实际使用体验来看,这个目标似乎未能达成。
距离 WWDC 26 已经不足十天了。相较于更多、更炫的新功能,我今年更期待苹果能给开发者和消费者带来一次脚踏实地、更加稳定的体验。
原创
用自定义 Layout 化解 SwiftUI List 的行高与间距跳变
在 SwiftUI 中,为视图状态变化添加动画往往只需要很少的代码,但 List 并不总能给出符合预期的过渡效果。尤其当 row 内部的内容高度发生变化时,例如副标题从无到有、文本行数因数据更新而改变,系统默认的布局过程很容易让行高直接硬切,进而带来闪烁、裁剪或 spacing 突变。本文从这一常见问题出发,逐步拆解 List 动态行高动画失效的原因,并通过自定义 Layout、Animatable、LayoutValueKey 与状态解耦,构建出一套完全基于 SwiftUI 原生能力的解决方案。
这并不是一篇以提供开箱即用组件为主要目标的文章。相比最终代码,我更希望借这个问题梳理一条 SwiftUI 布局问题的探查路径:先理解框架为什么“不按预期工作”,再顺着它的机制重新组织状态、测量与布局,让动画真正发生在正确的层级上。
近期推荐
无状态 actor (Stateless Actors)
Actor 通常被理解为“用来保护可变状态”的工具,因此一个没有存储属性、看似无状态的 actor 很容易让人疑惑:它到底有没有存在的必要?文章以 NetworkClient、自定义 global actor、custom executor 与文件系统访问等场景为例,说明无状态 actor 并非一定不合理,但也可能带来串行化、协议适配和类型系统传播等额外成本;关键在于它究竟是在解决什么问题。
这篇文章最值得关注的点,是 Matt Massicotte 提出的“actor 第一原则”:像使用任何同步原语一样,在使用 actor 之前,应该能清楚说明它为什么必要。无状态 actor 并不一定是错误,但它很可能意味着我们正在用 actor 解决一个并不需要 actor 的问题;只有当它确实承担了隔离、串行化、executor 适配或外部状态保护等明确职责时,才是合理的设计。
为 SwiftData 构建自定义存储格式 (Building a Custom Data Store in SwiftData)
很多开发者容易将 SwiftData 理解为苹果官方对 SQLite 的高层封装,但这其实低估了它作为对象图管理与持久化协调框架的潜力。无论是 SwiftData 还是 Core Data,更重要的能力都不只是替我们读写数据库,而是让模型对象、查询、上下文管理与底层存储之间保持清晰分工。只要底层存储能够按照框架要求完成数据交换,数据来源就不一定非得是 SQLite。
Mohammad Azam 通过实现一个基于 JSON 文件的 custom data store,展示了 SwiftData 与底层存储之间真正交换的并不是 @Model 标记的实时对象,而是经过转换后的 snapshot。理解这一点之后,DataStoreConfiguration、DataStore、DefaultSnapshot、PersistentIdentifier 以及 fetch / save 的职责边界都会变得清晰起来:SwiftData 负责模型对象、观察、变更追踪和 SwiftUI 集成,而自定义 store 只负责读取和写入快照。
在 #127 期周报中,我曾推荐过一个更工程化的自定义 DataStore 项目 DataStoreKit。它并不是简单地把 SwiftData 后端换成 JSON 或文件存储,而是尝试基于 custom DataStore 重新实现一套 SwiftData-aware 的 SQLite 存储层,并在 predicate 翻译、继承、缓存、历史追踪以及后台预取等方面做了大量扩展。想在此方向进一步研究的开发者,可以将它作为进阶参考。
给 Swift 并发任务命名 (Task Names in Swift Concurrency)
GCD 有 queue label,Swift Concurrency 的 task 却长期缺少类似的诊断标识。随着 SE-0469 在 Swift 6.2 中实现,Task、Task.detached、task group 的 addTask 等 API 开始支持 name 参数,开发者可以为并发任务添加简短、可读的名称,从而在 LLDB、Instruments 和日志中更容易定位具体执行单元。
Artem Novichkov 梳理了 task name 的用法与限制,并提醒开发者:task name 应只作为诊断信息,而不是程序逻辑的一部分。它适合辅助调试、性能分析和日志排查,但不应承载业务状态。
UniqueBox, Ref, and MutableRef in Swift 6.4
Swift 6.4 延续了近几年围绕 ownership、borrow、noncopyable type、lifetime dependency、Span 与 MutableSpan 等方向的演进。Artem Mirzabekian 通过 UniqueBox、Ref 和 MutableRef 这三个类型,介绍了 Swift 在存储位置、所有权与访问生命周期上的新表达能力。它们的意义并不是鼓励普通业务代码立刻改用这些底层构件,而是展示 Swift 正在把过去依赖 class box、UnsafePointer 或编译器内部推理的关系,逐步提升为可以写进 API 形状、并由类型系统检查的语言模型。
其中,UniqueBox 用来表达“一个值位于堆上,并由单一所有者持有”;Ref 和 MutableRef 则分别对应某段生命周期内的共享读取与独占修改。换句话说,这些类型补上的不是某个具体业务场景的便利 API,而是一套更精确描述“值在哪里、归谁所有、谁能访问”的底层词汇。
关于 CloudKit CKAsset 当前文件大小限制的澄清 (Clarification on Current CloudKit CKAsset File Size Limits)
关于 CKAsset 的文件大小上限,社区里长期存在一个容易混淆的说法:有些文档中提到过 50 MB 限制,但那更多对应 CloudKit Web Services,而不是 Apple 平台上通过原生 CloudKit framework 上传的 CKAsset。这次 Apple Frameworks Engineer 在开发者论坛中给出了一个非常明确的回答:单个 CKAsset 最高支持 50 GB,前提是用户仍有足够的 iCloud 存储空间,因此应用需要正确处理 CKError.quotaExceeded。
之所以在周报中介绍这个问答,并不只是因为它澄清了一个数字,而是因为它划清了一个重要边界:
CKAsset支持大文件,并不等于大文件同步就是简单可靠的工程问题。后续讨论也提到,大文件传输往往需要考虑应用挂起、终止后的后台执行能力,可以结合 long-lived CloudKit operations,或通过BackgroundTasks中的BGProcessingTask来安排上传任务。也就是说,50 GB 回答的是“CloudKit 是否允许”,而后台传输、失败恢复、配额处理和用户体验,仍然需要应用自己认真设计。
为 Agent 提供有效的调试信息 (Don’t allow the agent reading whole output of Xcodebuild)
随着 AI agent 越来越多地参与 Swift 项目的编译、测试和问题定位,一个很现实的问题开始变得突出:xcodebuild 的输出实在太长了。如果直接把完整日志交给 agent,不仅会消耗大量 token,也容易让真正有用的信息被淹没在噪声中。Lee young-jun 在本文中介绍的 Xcsift 正是为这个场景设计的工具:它不像 xcpretty 那样主要面向人类阅读,而是把 xcodebuild / SwiftPM 的输出整理成更适合 LLM 消费的结构化结果,例如编译错误、警告、测试失败、覆盖率和构建耗时等。
AI 辅助开发工作流中一个容易被忽略的细节是:我们不应该让 agent “读完所有内容再自己判断重点”,而应该尽量在工具层先完成信息筛选与结构化。Xcsift 是一个基于现有
xcodebuild输出的实用方案,而 Tuist 团队此前在 Teaching AI to Read Xcode Builds 中则进一步讨论了构建日志之外的结构化 build data。两者放在一起看,正好体现了 agentic coding 在构建诊断上的两个层次:先减少噪声,再提升语义。
The MiniSwift Story
我在之前的周报中曾推荐过 MiniSwift,当时最吸引人的部分,是 Ugur Toprakdeviren 在不依赖 LLVM、Clang 或 Apple 官方工具链的情况下,用 C 从零实现了 Swift 编译器前端与 WASM 后端。而在这篇文章中,作者补全了项目背后的来龙去脉:它最初并不是为了“炫技式地写一个编译器”,而是源于一个很具体的问题——能不能摆脱 WebView 或 Xcode Preview 的不稳定,用 canvas 得到更稳定的 SwiftUI 预览。
这次值得再次推荐,是因为 MiniSwift 已经明显从早期 compiler prototype 向它最初的目标迈进了一大步。现在它不只是把 Swift 代码编译到 WASM,而是进一步通过自定义 UIIR、canvas renderer 和 diff engine,把 SwiftUI 代码直接渲染成浏览器中的可交互预览;从官网展示来看,@State、基础布局、按钮、文本和部分修饰器已经可以在网页中形成类似 SwiftUI Preview 的即时反馈。也就是说,之前提到的“不会崩溃的 SwiftUI 浏览器预览”,正在从设想变成一个可以直接体验的工具。
工具
MistKit:让服务端 Swift 访问 CloudKit
由 Leo G Dion 开发的 MistKit 目标很明确:把 Apple 的 CloudKit Web Services REST API 封装成现代 Swift 接口,让服务端 Swift、命令行工具,以及 Linux、Windows 等没有原生 CloudKit framework 的环境,也能访问同一套 CloudKit 容器。它并不是要替代 Apple 平台上的 CloudKit framework,而是补足那些无法直接使用原生框架的场景。
项目基于 swift-openapi-generator 构建底层客户端,上层提供 async/await、类型安全的 CloudKit 操作、结构化错误、record 查询与增删改、record / zone changes、asset upload 和用户身份相关能力。认证方面覆盖 API Token、Web Auth Token 与 Server-to-Server;其中 Server-to-Server 主要用于 public database,涉及 private 或 shared database 的用户上下文操作则需要 Web Auth Token。这使它适合后台任务、CLI、内容目录、公共数据库管理,以及 Web 与 Apple 设备之间的数据桥接。
CloudKit 常被视为“客户端服务”,但 MistKit 让服务端参与 CloudKit 生态变得更自然。例如,用定时任务维护 public database 中的软件版本目录、RSS 聚合内容、应用素材包;或在用户授权后,让后端处理 private database 中的数据。对已经依赖 CloudKit 的应用来说,它提供了一条在 Apple 平台之外扩展数据处理能力的路径。