Fatbobman's Swift Weekly #144
When Every Data Write Comes at a New Price

When Every Data Write Comes at a New Price
Recently, OpenAI’s Codex was found to have a logging issue: the program kept writing large volumes of logs to a local database, causing the files to grow continuously and generating far more disk writes than normal. Bugs like this are hardly new. A few years ago, most people would probably have deleted the files, restarted the app, and waited for the next release to fix it.
This time, however, the reaction was noticeably different. Instead of asking how much disk space remained, more people began wondering how much data had already been written to their SSDs, whether it might affect flash endurance, and how much more they would have to pay for the same amount of memory and storage when replacing their devices in the future.
The Codex bug may not be any more serious than other runaway logging incidents in the past. What has changed is that every pointless write suddenly seems to come at a price again.
As demand for AI compute continues to surge, memory manufacturers are increasingly prioritizing higher-margin, higher-demand products such as HBM, server memory, and enterprise SSDs. Consumer memory and SSDs are therefore receiving a smaller share of production capacity, while their prices continue to rise.
Memory and flash have always been cyclical industries, so price increases and declines are nothing unusual. What makes the current situation unsettling is that it no longer appears to be merely another inventory cycle. It is accompanied by a longer-term shift of industry resources toward AI infrastructure.
Even if prices eventually fall, consumers may not quickly return to the old assumption that large amounts of storage are practically free.
As storage stops feeling inexhaustible, developers may also need to reconsider their attitude toward resources.
When I first encountered computers, a common Apple II configuration had only 48 KB of memory. To make use of the additional 16 KB provided by an expansion card, developers had to arrange addresses, code, and data with great care, finding ways to make every byte count.
A game spanning tens of gigabytes today inevitably offers graphics, sound, world scale, and interactive complexity that would have been unimaginable in the Famicom/NES era. Yet its enormous size does not guarantee that it will be more enjoyable than a game measured in only tens of kilobytes.
This is not to say that modern software should not grow larger. What is worth examining is how, over many years, developers have grown accustomed to storage capacities increasing naturally and networks becoming ever faster. As long as there is still room for logs, caches, and temporary data, few people stop to ask why they exist or when they will be deleted.
Expensive SSDs will not automatically produce better software, nor will memory shortages suddenly make developers smarter. But once hardware can no longer be assumed to grow without limit, engineering questions that have long been overlooked begin to resurface.
I do not miss the days when developers had to struggle within a few dozen kilobytes.
What is worth recovering is not the scarcity itself, but an engineering habit that once felt natural and has gradually faded away:
knowing why every byte arrives—and when it will leave.
Previous Issue|Newsletter Archive
📢 Sponsor Fatbobman’s Swift Weekly
Promote your product to Swift & iOS developers across:
- Blog: 50,000+ monthly visitors
- Newsletter: 4,000+ subscribers, 53% open rate
Perfect for developer tools, courses, and services.
Enjoyed this issue? Buy me a coffee ☕️
Recent Recommendations
Swift Task lifetime when an iOS device is locked
This Q&A comes from the Apple Developer Forums. The original poster found that a Swift Task making several sequential REST requests would sometimes complete normally after the device was locked, sometimes pause, and occasionally time out. DTS engineer Quinn “The Eskimo!” explains that Swift Concurrency runs entirely within the app process. Once iOS suspends that process, the worker threads executing the Task also stop, and the Task itself receives no additional background execution privileges.
This behavior can be especially confusing when networking is involved. If the app is suspended while a URLSession request is in progress, the request may fail because the connection becomes defunct. When the process resumes, developers may see a timeout or networking error rather than the Task continuing from where it paused and completing successfully. Quinn outlines four possible approaches: delay process suspension, stop in-flight requests before the app becomes eligible for suspension, allow requests to fail and implement retry and error recovery, or use a URLSession background session.
anyAppleOS in Swift 6.4
In the past, declaring availability for an API shared across Apple platforms required developers to list each platform and its corresponding OS version separately. Because version numbers differed across platforms for many years, these declarations were not only verbose but also prone to omissions and versioning mistakes. With Apple’s platforms adopting aligned major version numbers starting at 26, Swift 6.4 introduces anyAppleOS, allowing developers to describe APIs and code targeting Apple operating systems more concisely in @available, #available, and #if os(...).
Artem Mirzabekian introduces the new syntax and explains how it differs from platform-specific availability, canImport, and the platform declarations in Package.swift. He also shows how more specific availability rules can be combined with anyAppleOS when support arrives later on certain platforms or is unavailable altogether.
The Anatomy of a Reusable SwiftUI View
More developers are trying to make the APIs of their reusable SwiftUI views feel as close as possible to Apple’s own designs, but there is often no clear methodology for putting that goal into practice. Alexander Weiß shares his approach: first determine whether a component is semantic, describing a functional concept, or prescriptive, carrying a clearly defined visual form. From there, define its semantics, valid states, data flow, and interaction behavior. Components should also follow SwiftUI’s state-management conventions, use immutable values, Binding, and State appropriately, and treat accessibility, environment values, and stable view identity as part of the design rather than focusing only on the shape of the call site.
Using Card, Tag, and Rating as examples, the article demonstrates concrete implementations involving initializers, custom View Styles, and environment values. The central idea is not simply to imitate the appearance of Apple’s APIs, but to make custom components genuinely fit into the SwiftUI ecosystem through their composition model, state flow, interaction behavior, accessibility support, and environmental adaptation.
The hidden cost of unstable SwiftUI environment defaults
The @Entry macro makes custom environment values easier to declare, but directly using a reference-type instance as the default, such as @Entry var logger = AppLogger(), can cause unnecessary view updates. Whenever SwiftUI cannot find an injected value higher in the hierarchy and falls back to the default, it may create a new AppLogger. Although these objects behave identically, they are not the same instance, so SwiftUI may treat the environment value as changed and re-evaluate views that read it. Starting with Xcode 27, the compiler warns about this pattern, making a previously subtle issue easier to catch.
Natalia Panferova uses _printChanges() to demonstrate the issue directly and offers two solutions: keep the default instance in stable external storage, or declare the environment value as Optional and inject it explicitly at the app entry point.
I also analyzed this issue in SwiftUI Environment: Concepts and Practice. The root cause lies in how the @Entry macro generates the default environment value, allowing the default expression to be evaluated repeatedly whenever fallback access occurs.
Make Vertical Parallax for ScrollView in SwiftUI
Early 2D games created rich visual depth at very little cost by moving different image layers at different speeds. In SwiftUI, however, the difficult part of implementing scroll-based parallax is usually not calculating the offset, but continuously obtaining each view’s position within a ScrollView. In the past, this often required a combination of GeometryReader, custom coordinate spaces, and state propagation, resulting in more code and potentially causing unnecessary view updates when handled poorly.
The introduction of visualEffect has greatly reduced the complexity of building these effects. codelaby demonstrates how to read a view’s geometry directly inside visualEffect and apply an offset based on its scroll position, without maintaining additional state or changing the view’s original layout.
Tools
swift-span-algorithms: Adding Algorithmic Capabilities to Span
When working with contiguous memory, Swift developers traditionally had two main choices. Types such as Array and ContiguousArray own their storage and are safe and convenient, but may involve copying, reference counting, or additional allocation. Pointer types such as UnsafeBufferPointer offer low overhead and greater control, but require developers to ensure that the underlying memory remains valid and that every access stays within bounds.
Introduced in Swift 6.2, Span<Element> fills the gap between these approaches. It is a non-owning view over contiguous memory: it does not retain the underlying storage, but relies on compiler-enforced lifetime tracking and bounds checks to provide safe access. In Swift 6.2, however, Span does not yet offer the same breadth of algorithms available to Collection and Sequence.
David Retegan created swift-span-algorithms to fill that gap. The library adds searching, comparison, trimming, chunking, sliding windows, splitting, cursor-based operations, and other utilities for Span, RawSpan, MutableSpan, and InlineArray. It is designed around zero hidden allocations and constant additional space, extending Span’s algorithmic capabilities while continuing to respect its lifetime constraints.
Thanks for reading Fatbobman’s Swift Weekly! This post is public so feel free to share it.
当每一次写入都有了新价格
最近,OpenAI 的 Codex 被发现存在一个日志写入问题:程序会持续向本地数据库写入大量日志,导致文件不断膨胀,并产生远超正常水平的磁盘写入。类似的 Bug 过去并不少见。放在几年前,人们大概只是删除文件、重启应用,然后等待下一个版本修复。
但这一次,大家的反应明显不同。比起“还剩多少磁盘空间”,更多人开始关心 SSD 已经累计写入了多少数据,会不会影响闪存寿命,以及未来更换设备时,需要为同样容量的内存和存储多付多少钱。
Codex 的 Bug 未必比过去那些失控的日志程序更加严重。真正发生变化的是:每一次无意义的写入,似乎都重新有了价格。
随着 AI 算力需求迅速增长,存储器厂商越来越倾向于优先保障 HBM、服务器内存和企业级 SSD 等利润更高、需求更旺盛的产品。消费级内存和 SSD 能够分配到的产能因此受到挤压,价格也不断走高。
内存和闪存本来就是典型的周期性行业,价格上涨和下跌并不罕见。但这一轮变化令人不安的地方,在于它似乎不再只是一次简单的库存周期,而是伴随着产业资源向 AI 基础设施长期倾斜。
即使价格最终仍会回落,消费者恐怕也很难迅速回到过去那种“大容量近乎免费”的心理预期。
当存储不再显得取之不尽时,开发者或许也应该转变对资源时的态度。
在我最开始接触电脑时,Apple II 常见的内存配置只有 48 KB。为了使用扩展卡额外提供的 16 KB 空间,开发者需要仔细安排地址、代码和数据,想方设法把每一个字节都利用起来。
今天一个数十 GB 的游戏,必然拥有 FC 时代无法想象的画面、声音、世界规模和交互复杂度。然而,巨大的容量并不能保证它带来的乐趣一定超过一个只有几十 KB 的游戏。
这并不是说现代软件不应该变大。真正值得思考的是,在过去很多年中,开发者已经习惯了存储容量会自然增长、网络会不断变快。日志、缓存和临时数据只要暂时还放得下,就很少有人追问它们为什么存在,又将在什么时候被删除。
昂贵的 SSD 不会自动创造优秀的软件,内存短缺也不会让开发者突然变得更加聪明。但当硬件不再被默认会无限增长时,那些曾经被忽视的工程问题便会重新浮出水面。
我并不怀念那个需要在几十 KB 中反复腾挪的时代。
真正值得重新拾起的,并不是资源匮乏,而是那种曾经十分自然、后来却逐渐消失的工程习惯:
知道每一个字节为什么到来,也知道它将在什么时候离开。
如果您发现这份周报或我的博客对您有所帮助,可以考虑通过 Buy Me a Coffee 支持我的创作。
近期推荐
iOS 设备锁定后,Swift Task 能运行多久?(Swift Task lifetime when an iOS device is locked)
这是一篇来自 Apple Developer Forums 的问答。提问者发现,一个会串行执行多个 REST 请求的 Swift Task,在设备锁定后有时能够正常完成,有时会暂停,甚至出现超时。DTS 工程师 Quinn “The Eskimo!” 解释道,Swift Concurrency 完全运行在应用进程中;一旦 iOS 挂起进程,执行 Task 的工作线程也会停止,Task 本身并不会获得额外的后台运行能力。
这种机制在网络请求中尤其容易造成困惑。应用被挂起时,正在进行的 URLSession 请求可能因连接失效而失败;进程恢复后,开发者看到的可能是请求超时或网络错误,而不是 Task 从暂停处继续并顺利完成。Quinn 给出了四种处理方向:设法延缓进程挂起、在应用即将被挂起时停止请求、允许请求失败并做好重试与错误恢复,或者使用 URLSession 的后台会话。
Swift 6.4 中的 anyAppleOS (anyAppleOS in Swift 6.4)
过去,为跨 Apple 平台共享的 API 声明可用性时,开发者需要分别列出各个平台及其对应的系统版本。由于不同平台的版本号长期不一致,这类声明不仅冗长,也容易出现遗漏或版本填写错误。随着 Apple 各平台从 26 开始统一版本号,Swift 6.4 引入了 anyAppleOS,允许开发者在 @available、#available 和 #if os(...) 中,以更简洁的方式描述适用于 Apple 操作系统的 API 与代码。
Artem Mirzabekian 介绍了这一新语法,并梳理了它与平台专属可用性、canImport 以及 Package.swift 平台声明之间的区别,同时说明了如何结合更具体的可用性规则,处理部分平台延后支持或完全不可用的情况。
可复用 SwiftUI 视图的设计剖析 (The Anatomy of a Reusable SwiftUI View)
越来越多的开发者在构建可复用 SwiftUI 视图时,都希望其 API 尽可能贴近 Apple 官方的设计方式,但具体如何落地,往往缺少一套清晰的方法。Alexander Weiß 给出了自己的设计思路:首先判断组件属于描述功能概念的语义型组件,还是具有明确视觉形态的规定型组件,再梳理其语义、合法状态、数据流与交互行为。同时,组件还应遵循 SwiftUI 的状态管理方式,正确使用不可变值、Binding 与 State,并将无障碍支持、环境值和视图身份稳定性纳入设计,而不只是关注 API 的调用形式。
文章通过 Card、Tag 和 Rating 三个示例展示了初始化方法、自定义 View Style 和环境值等具体实现。这套方法的核心并非简单模仿 Apple API 的外形,而是让自定义组件在组合方式、状态流转、交互行为、无障碍支持和环境适配等方面真正融入 SwiftUI 体系。
SwiftUI 环境默认值不稳定的隐性成本 (The hidden cost of unstable SwiftUI environment defaults)
@Entry 宏让自定义环境值的声明更加便捷,但如果直接将引用类型实例作为默认值,例如 @Entry var logger = AppLogger(),就可能带来不必要的视图更新。每当 SwiftUI 没有找到上层注入的值而回退到默认值时,都可能重新创建一个 AppLogger。这些对象虽然功能相同,却不是同一个实例,因此 SwiftUI 会认为环境值发生了变化,进而重新计算读取它的视图。Xcode 27 开始会针对这种写法发出警告,让这个过去不易察觉的问题更早暴露出来。
Natalia Panferova 使用 _printChanges() 直观展示了这一问题,并给出两种解决方式:将默认实例保存在稳定的外部存储中,或者将环境值声明为 Optional,再由应用入口显式注入。
我在《SwiftUI Environment:理念与实践》中也分析过这一问题。其根源在于
@Entry宏生成默认环境值的方式,使默认表达式可能在回退访问时被重复求值。
使用 visualEffect 为 SwiftUI ScrollView 创建纵向视差效果 (Make Vertical Parallax for ScrollView in SwiftUI)
早期的二维游戏通过让不同图层以不同速度移动,用很低的成本便营造出了富有层次感的视觉效果。但在 SwiftUI 中实现滚动视差,真正麻烦的通常不是计算偏移量,而是持续获取每个视图在 ScrollView 中的位置。过去,这类效果往往需要结合 GeometryReader、自定义坐标空间和状态传递来完成,不仅代码量较大,处理不当还可能引发额外的视图更新。
visualEffect 的引入大幅降低了这类效果的实现难度。codelaby 展示了如何直接在 visualEffect 中读取视图的几何信息,并根据滚动位置应用 offset,无须额外维护状态,也不会改变视图原有的布局。
工具
swift-span-algorithms: 给 Span 补上算法能力
在处理连续内存时,Swift 过去主要有两类选择:使用 Array、ContiguousArray 等拥有存储的容器,安全易用,但可能伴随复制、引用计数或额外分配;使用 UnsafeBufferPointer 等指针类型,成本低、控制力强,却需要开发者自行保证内存仍然有效,并避免越界访问。
Swift 6.2 新增的 Span<Element> 填补了两者之间的空白。它是一段连续内存的非拥有视图:不持有底层存储,却能借助编译器追踪生命周期,并通过边界检查提供安全访问。不过,在 Swift 6.2 中,Span 尚未拥有 Collection、Sequence 那般完整的算法能力。
David Retegan 开发的 swift-span-algorithms 正是对这一缺口的补充。它为 Span、RawSpan、MutableSpan 和 InlineArray 提供查找、比较、修剪、分块、滑动窗口、切分及游标(Cursor)等操作。整个库零以无隐藏分配和常数级额外空间为设计目标,在扩展算法能力的同时,仍然遵守 Span 的生命周期约束。

