普通视图
AT 的人生未必比 MT 更好 -- 肘子的 Swift 周报 #118
![]()
AT 的人生未必比 MT 更好
学车时我开的是手动挡,起初因为技术生疏,常搞得手忙脚乱,所以第一台车就直接选了自动挡。但开了几年,我开始追求那种完全掌控的驾驶感,于是又增购了一台手动挡。遗憾的是,随着交通日益拥堵,换挡的乐趣逐渐被疲惫抵消,最终这台车也被冷落。算起来,我已经快二十年没认真开过手动挡了,但内心深处,我仍会时不时地怀念那段“人车合一”的时光。
随着 AI 深度介入我的工作与生活,我感觉自己的人生正从 MT 转向 AT。毫无疑问,AI 助我突破了许多能力瓶颈,也在熟悉领域带来了巨大的效率提升。但奇怪的是,我对它的“惊叹”却在与日俱减。看似它节省了我的时间,但我并未从这些“多出来的时间”里获得预期的满足感。也许是我对它的期望阈值不断提高,但一个不争的事实是:我已经有一段时间没有在学习和开发过程中,体会到那种单纯的快乐了。
幸好,几天前我又找回了这种久违的开心。在准备 iOS Conf SG 的 Keynote 时,由于 Keynote 在动画构建上相对“原始”且缺乏 AI 辅助,我难得地拥有一段完整的时间,去纯手工地尝试和修改。哪怕只是一个简单的并行动画,究竟是用转场、普通动画还是“神奇移动”,我都玩得不亦乐乎。那些在专家手里可能两三分钟搞定的设置,我折腾了大半天。尽管毫无效率可言,但我乐在其中。看着最终那个略显“简陋”的效果,我竟被自己感动了。
对于职场中人,效率和完成度固然是硬指标;但能否体会到过程带来的“实感”,或许才是作为“人”最朴素的追求。
我大概率不会再买 MT 的车了,但在我还能握紧方向盘的时候,也不会轻易让“智能驾驶”代劳。写代码也是如此,当初爱上它,正是因为它能给我带来纯粹的快乐。当所有工具都在催促我们变得更快更好时,我想应该给自己留一点变慢、变“笨”的空间——毕竟,AT 的人生未必比 MT 更好。
🚀 《肘子的 Swift 周报》
每周为你精选最值得关注的 Swift、SwiftUI 技术动态
- 📮 立即订阅 | weekly.fatbobman.com 获取完整内容
- 👥 加入社区 | Discord 与 2000+ 开发者交流
- 📚 深度教程 | fatbobman.com 探索 200+ 原创文章
近期推荐
告别“可移植汇编”:我已让 Swift 在 MCU 上运行七年
从 2024 年开始,Swift 官方正式提供了对嵌入式系统的支持,但要在这个领域获得显著份额,仍有很长的路要走。其实,早在官方下场之前的 2018 年,Andy Liu 和他的 MadMachine 团队就开始在探索和实践将 Swift 应用于嵌入式领域,并陆续推出了相关硬件。他们坚信,在功能日益复杂的开发场景中,Swift 的现代语言特性将展现出巨大的优势。在本文中,Andy 分享了过去几年中在该领域的探索历程。我真心希望,Swift 能够在更多的场景中,展现其魅力。
在 SwiftUI 中构建基础拨号滑块组件 (Building a Base DialSlider Component in SwiftUI)
在 AI 功能越来越强大的今天,看到一个动效,让 AI 帮你实现已经越来越容易了。但每当看到开发者凭借自己的"奇思妙想"不断探索实现方式并打磨效果时,我仍会由衷赞叹。codelaby 在这篇复刻"老式电话拨号盘"的文章中,巧妙运用 SwiftUI 的 .compositingGroup() 和 .blendMode(.destinationOut) 实现了动态"镂空"效果,使旋转层下的静态数字清晰显现,相比单纯旋转图片更具灵活性和原生质感。此外,文章对环形手势处理、角度计算(atan2)以及限位逻辑(Stopper)的阐述也十分透彻清晰。
CKSyncEngine 答疑与实战经验分享 (CKSyncEngine Questions and Answers)
不少开发者对 Core Data 的 NSPersistentCloudKitContainer 颇有诟病,认为其不透明且缺乏定制性。但真正想自己着手解决 CloudKit 的数据同步问题时,才发现需要考虑的地方实在太多,难度远超想象。苹果在几年前推出的 CKSyncEngine 彻底打破了这个困境,提供了更清晰的状态管理和错误处理,并自动处理了诸多复杂的边缘情况,让开发者可以专心构建数据同步逻辑。
Christian Selig 通过自问自答的方式,分享了他在使用 CKSyncEngine API 时的经验,详细拆解了 CKSyncEngine 如何作为一个完美的中间层,在保留数据存储灵活性(你可以继续用 SQLite、Realm 或 JSON)的同时,接管了最令人头疼的同步状态管理。
我对 iOS 26 Tab Bar 的吐槽 (My Beef with the iOS 26 Tab Bar)
SwiftUI 在 iOS 18 中对 Tab Bar 的调整,其影响几乎堪比当年 NavigationStack/NavigationSplitView 取代 NavigationView,不仅改变了设计语言,对应用的实现方式和数据流走向都产生了颠覆性影响。而为 iOS 26 Liquid Glass 风格引入的"搜索标签"功能进一步推进了这种变革。Ryan Ashcraft 在这篇文章中直言不讳地指出了新 Tab Bar 设计的诸多问题:默认的浮动样式在某些界面中显得突兀,与应用整体视觉风格难以协调;新的边距和间距规则打破了长期以来的设计惯例,让开发者需要重新调整大量现有界面;更重要的是,这些改动并未明显提升用户体验,反而增加了适配成本。
我个人对新 Tab 的最大感受是,它会显著影响开发者在开发应用时对最低系统版本的决策。为 Tab 维护两套代码是否值得?如果为了简化实现而不得不将最低版本提高到 iOS 18,这或许正是 SwiftUI 团队的另一个设计意图?
Dia:深度剖析 The Browser Company 的 macOS 浏览器架构 (Dia: A Technical Deep Dive into The Browser Company's macOS Browser)
Arc 是第一个使用 Swift 构建的大型 Windows 平台应用,而且 The Browser Company 也因此为 Swift 社区的 Windows 工具链做出了突出贡献。在从 Arc 转型到 Dia 后,开发团队并没有放弃使用 Swift,那么 macOS 端的 Dia 具体使用了哪些开发框架呢?
Everett 在本文中揭示了 Dia 独特的技术架构:这是一个基于 AppKit + SwiftUI 的原生 macOS 应用,但其核心渲染引擎并非 WebKit,而是嵌入了自行修改的 Chromium(ArcCore)。此外,在 Dia 的二进制文件中发现了大量与本地 AI 相关的库(如 Apple MLX 和 LoRA 适配器),这预示着 Dia 并非只是为了"快",而是已经为设备端 AI 推理做好了底层工程准备。
关于罗技开发者证书过期的迷思 (Myths about Logitech Developer ID certificate expiration)
几天前,不少 macOS 用户发现罗技鼠标的自定义按钮失效。由于控制台日志中充斥着代码签名(Code Signing)相关的报错,不少用户和媒体将其归咎于"苹果撤销了证书"。Jeff Johnson 通过分析系统日志为苹果在本次事件中的角色进行了平反:这并非苹果的证书服务故障,而是罗技自身软件工程问题导致的。Logi Options+ 的后台进程在更新后,未能通过 macOS taskgated 的代码签名有效性验证,从而被系统直接终止。这篇文章不仅是一份故障分析报告,更提醒开发者:在 macOS 严格的安全机制下,应用更新的签名验证流程容不得半点马虎。
“如果你的证书过期了,用户仍然可以下载、安装和运行用该证书签名的 Mac 应用程序版本。但是,你需要一个新的证书来签署更新和新申请。” —— 苹果官方文档
拒绝 LLM 生成的平庸代码 (Stop Getting Average Code from Your LLM)
不可否认,在人类长久以来累积的信息海洋中,高质量的数据与信息只占少数。对于个体来说,我们完全可以有目的地去甄别和学习这些优质内容。但是,受限于机制,LLM 默认倾向于训练数据的“平均值”,这就导致它生成的内容在各个方面都显得比较平庸。具体到 Swift 开发领域,这往往意味着它会生成大量旧版的、非结构化的代码。
要想获得高质量、符合 Swift 6 标准甚至特定架构风格的代码,关键在于对抗这种“回归均值”的本能。Krzysztof Zabłocki 详细介绍了如何利用 Few-Shot Prompting(少样本提示)和上下文注入技术,通过提供具体的代码范例和架构规范,强迫 LLM “忘记”平庸的默认设置,转而生成精准匹配项目标准的高质量代码。
工具
swift-effect: 一种基于类型驱动的副作用处理方案
Alex Ozun 长期关注 Swift 中的 类型驱动设计,这个库是他对“代数效应(Algebraic Effects)+ 处理器(Effect Handlers)”在 Swift 里的实践。 swift-effect 不是把副作用变成“数据结构再解释”,而是将副作用建模为可拦截的全局操作(@Effect),通过 handler 在运行时组合和替换行为,让业务代码保持线性/过程式风格,同时又能精细控制 I/O、并发等行为。
核心亮点:
- 保持代码线性:调用 Console.print 等 effect 就像普通函数,但行为可由 handler 动态决定。
-
无 Mock 的行为测试:用
withTestHandler逐步拦截/断言 effect 序列,像“交互式脚本”一样测试流程。 - 并发可控:支持对 Task/AsyncSequence 的确定性测试,解决并发顺序不稳定的问题。
Codex Skill Manager: 一款面向众多 CLI 的 macOS 工具
很多开发者都会同时使用多种 AI 编程服务,尽管它们拥有类似的概念、设定和工具类型,但在具体设置和细节描述上仍有差异,这导致开发者很难对所有服务进行统一管理。Thomas Ricouard 开发的 Codex Skill Manager 将 Codex、Claude Code(以及 OpenCode、Copilot)的技能集中在一个 UI 里查看、搜索、删除和导入,避免在多个隐藏目录中手动寻找。
核心功能
-
本地技能:扫描
~/.codex/skills/public、~/.claude/skills等路径,展示列表与详情 - 详情渲染:Markdown 视图+引用预览
- 远程 Skill:Clawdhub 搜索/最新列表、详情拉取与下载
- 导入/删除/自定义路径:支持从 zip 或文件夹导入、侧边栏删除、添加自定义路径
- 多平台安装状态:为不同平台标记已安装状态
活动
LET'S VISION 2026|邀请你与我们同行!
✨ 大会主题:Born to Create, Powered by AI
- 📍 地点:上海漕河泾会议中心
- ⏰ 时间:2026 年 3 月 27 日 - 3 月 29 日
- 🏁 重点:汇聚顶尖创作者与 AI 技术大咖,共同探索 AI 应用的未来边界
- 🌍 官网:letsvision.swiftgg.team
别走开!请关注官方账号和主理人 SwiftSIQI,我们将持续放送更多精彩内容!
Swift Student Challenge 2026
每年一度的学生挑战赛再次登场。挑战赛为数以千计的学生开发者提供了展现创造力和编程能力的机会,让他们可以通过 App Playground 呈现自己的作品,并从中学习在职业生涯中受用的实际技能。
今年作品提交通道将于 2026 年 2 月 6 日至 2 月 28 日开放。
往期内容
- 2026:当 AI 隐入工作流,你准备好了吗?- #117
- Swift、SwiftUI 与 SwiftData:走向成熟的 2025 - #116
- 周日小插曲 - #115
- 挖掘“沉默的专家”- #114
💝 支持与反馈
如果本期周报对你有帮助,请:
- 👍 点赞 - 让更多开发者看到
- 💬 评论 - 分享你的看法或问题
- 🔄 转发 - 帮助同行共同成长
🚀 拓展 Swift 视野
- 📮 邮件订阅 | weekly.fatbobman.com 获取独家技术洞察
- 👥 开发者社区 | Discord 实时交流开发经验
- 📚 原创教程 | fatbobman.com 学习 Swift/SwiftUI 最佳实践
AT 的人生未必比 MT 更好 - 肘子的 Swift 周报 #118
学车时我开的是手动挡,起初因为技术生疏,常搞得手忙脚乱,所以第一台车就直接选了自动挡。但开了几年,我开始追求那种完全掌控的驾驶感,于是又增购了一台手动挡。遗憾的是,随着交通日益拥堵,换挡的乐趣逐渐被疲惫抵消,最终这台车也被冷落。算起来,我已经快二十年没认真开过手动挡了,但内心深处,我仍会时不时地怀念那段“人车合一”的时光。
告别“可移植汇编”:我已让 Swift 在 MCU 上运行七年
在苹果官方正式开启嵌入式支持之前,Andy Liu 和他的 MadMachine 团队就已经在这个领域深耕多年。他们认为,在功能日益复杂的开发场景中,Swift 的现代语言特性将展现出巨大的优势。在数年前便选择了一套与社区主流不同的理念与技术路线。 我邀请 Andy 分享他们过去几年在 Swift 嵌入式开发中的实战经历分享出来。这既是一份宝贵的历史记录,也希望能为社区提供一个不一样的思考维度。
Swift 6.2 列传(第十六篇):阿朱的“易容术”与阿紫的“毒药测试”
拒绝“假死”:为何上滑关闭是测试大忌?揭秘 iOS 真实 OOM 触发指南
Core Data 简化开发:NSPersistentContainer 从原理到实战
作为 iOS/macOS 开发者,本地数据存储是绕不开的话题。提起 Core Data,不少新手会皱眉头 —— 早期的 Core Data 配置繁琐,手动管理上下文、协调器这些组件很容易踩坑;而老开发者则清楚,自从 Apple 推出NSPersistentContainer后,Core Data 的使用体验直接 “起飞”。今天就跟大家聊聊,这个 “容器” 到底是什么、怎么用,以及它的那些优缺点。
一、先唠唠 Core Data 的 “老痛点”
在 iOS 10/macOS 10.12 之前,想用 Core Data 得手动搭一套 “流水线”:
- 加载
NSManagedObjectModel(数据模型); - 创建
NSPersistentStoreCoordinator(持久化存储协调器),指定存储类型(比如 SQLite)和路径; - 实例化
NSManagedObjectContext(托管对象上下文),并关联协调器; - 还要处理线程安全、上下文合并这些问题。
一套操作下来,代码又长又容易出错,光是初始化就能劝退一半新手。Apple 显然也发现了这个问题,于是NSPersistentContainer应运而生 —— 它把 Core Data 的核心组件全 “打包” 了,让我们不用再关心底层细节,专注于业务逻辑即可。
二、NSPersistentContainer:Core Data 的 “一站式工具箱”
1. 核心原理:封装了什么?
NSPersistentContainer本质是对 Core Data 三大核心组件的封装,相当于给我们准备了一个开箱即用的 “数据管理容器”,内部结构如下:
| 组件 | 作用 | 容器中的访问方式 |
|---|---|---|
NSManagedObjectModel |
定义数据结构(对应.xcdatamodeld 文件) | container.managedObjectModel |
NSPersistentStoreCoordinator |
管理数据存储(比如 SQLite 文件) | container.persistentStoreCoordinator |
NSManagedObjectContext |
操作数据的 “工作台”(增删改查) |
container.viewContext(主线程)/container.newBackgroundContext()(后台) |
简单说:你只需要告诉容器 “数据模型叫什么名字”,它会自动完成模型加载、协调器创建、上下文关联等所有底层工作,不用写一行冗余代码。
2. 最核心的两个上下文
容器里最常用的是两个上下文,一定要分清:
- viewContext:默认绑定主线程,专门用于 UI 相关的操作(比如列表展示读书笔记),线程安全,直接用就行;
- newBackgroundContext() :每次调用都会生成一个新的后台上下文,用于耗时操作(比如批量导入历史读书笔记),避免阻塞主线程导致 UI 卡顿。
三、实战:NSPersistentContainer 的基本用法
光说不练假把式,我们用一个简单的 “读书笔记管理” 示例,看看怎么用容器搞定 Core Data 的增删改查。
前置准备
-
创建 iOS 项目时勾选「Use Core Data」(Xcode 会自动生成基础的容器代码);
-
打开
.xcdatamodeld文件,创建一个BookNote实体,添加三个属性:-
bookName(String,书名); -
content(String,笔记内容); -
createTime(Date,创建时间,默认值可设为@now)。
-
1. 初始化容器(Xcode 自动生成,稍作优化)
AppDelegate 中的核心代码,我们优化下错误处理(别用 fatalError,实际项目要友好):
import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// 懒加载持久化容器
lazy var persistentContainer: NSPersistentContainer = {
// 模型文件名要和.xcdatamodeld文件名称一致(比如我命名为BookNoteModel)
let container = NSPersistentContainer(name: "BookNoteModel")
// 加载持久化存储(默认是SQLite文件,存储在App沙盒中)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// 实际项目中替换为日志/弹窗提示,别直接崩溃
print("Core Data加载失败:(error.localizedDescription)")
}
})
return container
}()
// 封装保存上下文的方法,复用性更高
func saveContext() {
let context = persistentContainer.viewContext
guard context.hasChanges else { return } // 没有修改就不保存,减少IO消耗
do {
try context.save()
print("读书笔记保存成功✅")
} catch {
print("保存失败❌:(error.localizedDescription)")
}
}
}
2. 增删改查实战(ViewController 中)
import UIKit
import CoreData
class ViewController: UIViewController {
// 获取容器(实际项目建议用单例/依赖注入,别直接强转AppDelegate,这里为了简化)
private var container: NSPersistentContainer {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer
}
// 1. 添加读书笔记
@IBAction func addBookNote(_ sender: UIButton) {
let context = container.viewContext
// 创建BookNote对象
let note = BookNote(context: context)
note.bookName = "《小王子》"
note.content = "正是你为你的玫瑰花费的时光,才使你的玫瑰变得如此重要。"
note.createTime = Date() // 也可以依赖模型的默认值,这里手动赋值更直观
// 调用AppDelegate的保存方法
(UIApplication.shared.delegate as! AppDelegate).saveContext()
}
// 2. 查询所有读书笔记(可按创建时间倒序)
func fetchAllBookNotes() {
let context = container.viewContext
// 创建查询请求
let fetchRequest: NSFetchRequest<BookNote> = BookNote.fetchRequest()
// 按创建时间倒序排列,最新的笔记在前面
let sortDescriptor = NSSortDescriptor(keyPath: \BookNote.createTime, ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let notes = try context.fetch(fetchRequest)
notes.forEach { note in
print("📚 书名:(note.bookName ?? "未知")")
print("✍️ 笔记:(note.content ?? "无内容")")
print("🕒 创建时间:(note.createTime ?? Date())\n")
}
} catch {
print("查询读书笔记失败:(error.localizedDescription)")
}
}
// 3. 删除读书笔记(示例:删除第一条《小王子》的笔记)
@IBAction func deleteBookNote(_ sender: UIButton) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<BookNote> = BookNote.fetchRequest()
// 增加筛选条件:只删《小王子》的笔记
fetchRequest.predicate = NSPredicate(format: "bookName == %@", "《小王子》")
do {
if let targetNote = try context.fetch(fetchRequest).first {
context.delete(targetNote) // 删除指定笔记对象
(UIApplication.shared.delegate as! AppDelegate).saveContext()
print("《小王子》的笔记已删除")
}
} catch {
print("删除读书笔记失败:(error.localizedDescription)")
}
}
// 4. 后台批量导入读书笔记(重点:用后台上下文,不卡UI)
func batchImportBookNotes() {
// 创建后台上下文
let backgroundContext = container.newBackgroundContext()
// 设置合并策略,避免多上下文操作冲突
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// 在后台线程执行批量操作,不会阻塞主线程
backgroundContext.perform { [weak self] in
// 模拟批量导入3本经典书籍的笔记
let noteDatas = [
("《百年孤独》", "生命中真正重要的不是你遭遇了什么,而是你记住了哪些事,又是如何铭记的。"),
("《解忧杂货店》", "其实所有纠结做选择的人心里早就有了答案,咨询只是想得到内心所倾向的选择。"),
("《活着》", "人是为了活着本身而活着的,而不是为了活着之外的任何事物而活着。")
]
// 循环创建笔记对象
for (bookName, content) in noteDatas {
let note = BookNote(context: backgroundContext)
note.bookName = bookName
note.content = content
note.createTime = Date()
}
// 保存后台上下文的修改
do {
try backgroundContext.save()
print("批量导入读书笔记完成✅")
} catch {
print("批量导入失败❌:(error.localizedDescription)")
}
}
}
}
小提示
- 别在主线程做批量导入 / 大量查询操作!一定要用
newBackgroundContext(); - 后台上下文的
perform方法会自动在对应的后台线程执行代码,不用手动写 GCD(比如DispatchQueue.global().async); - 实际项目中,建议把 Core Data 操作封装成单独的工具类(比如
BookNoteManager),把增删改查的逻辑抽离出来,ViewController 只负责调用,代码更整洁易维护。
四、NSPersistentContainer 的优缺点
优点:新手友好,效率拉满
- 极大简化配置:不用手动管理模型、协调器、上下文的关联,几行代码就能初始化 Core Data;
-
线程安全:
viewContext默认绑定主线程,避免了新手最容易踩的 “线程混乱” 坑; - 易于扩展:支持自定义存储路径、存储类型(比如内存存储,适合测试),满足进阶需求;
- 官方维护:Apple 持续优化,兼容性和稳定性有保障。
缺点:灵活度略有妥协
- 底层封装过深:新手可能只知其然不知其所以然,遇到复杂问题(比如跨版本数据迁移)时,排查起来比较费劲;
- 自定义配置稍麻烦:如果要修改默认的存储路径、缓存大小,需要额外写代码拆解容器;
- 不支持跨平台(纯 Swift) :Core Data 是 Apple 专属框架,如果你想做跨平台 App(比如 iOS+Android),还是得用 Realm、SQLite.swift 等。
五、最后聊聊:什么时候用 NSPersistentContainer?
- ✅ 推荐用:绝大多数常规 App(比如读书笔记、备忘录、待办清单类),只需要本地存储数据,不需要复杂的自定义配置;
- ❌ 谨慎用:如果你需要深度定制 Core Data 的底层(比如自定义存储协调器、复杂的数据迁移策略),可能需要结合底层 API 使用;
- 📌 替代方案:如果追求跨平台 / 纯 Swift,可考虑 Realm、GRDB.swift;如果数据量极小,直接用 UserDefaults 就行。
总结
-
NSPersistentContainer是 Apple 为简化 Core Data 开发推出的 “利器”,封装了 Core Data 的核心组件,iOS 10 + 可直接用; - 核心用法:初始化容器→用
viewContext处理 UI 相关操作(比如展示读书笔记)→用newBackgroundContext()处理后台耗时操作(比如批量导入)→保存上下文; - 它的优点是简单、安全、高效,缺点是底层封装过深,灵活度略有妥协,适合绝大多数常规 iOS/macOS 项目。
Core Data 看似复杂,但有了NSPersistentContainer这个 “帮手”,新手也能快速上手。与其纠结底层原理,不如先动手写起来,遇到问题再深入研究,毕竟实践才是最好的老师~
关键点回顾
- 示例场景替换为「读书笔记管理」,实体
BookNote包含bookName(书名)、content(笔记内容)、createTime(创建时间)三个核心属性; - 核心代码逻辑不变,但所有增删改查、批量导入的操作都围绕 “读书笔记” 展开,更贴近日常开发场景;
- 保留了原博客轻松的语气和完整的讲解结构,同时补充了排序、筛选等更实用的查询技巧。
2026:当 AI 隐入工作流,你准备好了吗? -- 肘子的 Swift 周报 #117
![]()
2026:当 AI 隐入工作流,你准备好了吗?
大家新年好!在过去的几年中,AI 始终占据着科技界最耀眼的 C 位。但站在 2026 年的起点回看,我发现一个显著的转折:从 2025 年末开始,人们对“万亿参数”或“榜单跑分”的狂热逐渐褪去,取而代之的是对 AI 工作流深耕细作的冷静与实战。
如果说过去两年大多数人还在尝试如何与 Chat 机器人聊天,那么现在,AI 已经通过 CLI、MCP 以及各种 Slash、Skill、SubAgent,彻底打破了对话框的限制。对于有经验的开发者来说,AI 已经不再是一个外部工具,而是像插件一样,渗透进终端、编辑器乃至整个操作系统的每一个毛细血管。
在这一点上,macOS 展示了某种“无心插柳”的天然优势。借助 AppleScript 和快捷指令这些成熟的自动化工具,即便不通过复杂的 API 开发,普通用户也能让 AI 访问自己的私有数据。这种“老树发新芽”的现象,让苹果在 AI 时代拥有了新的护城河。而如果这种能力在 iOS 上通过系统级 Agent 完全释放,硬件设备的形态或许将迎来新一轮重塑。
与此同时,某些厂商的策略则更加“激进”。字节跳动的豆包手机尝试从系统底层通过屏幕读取与模拟交互来“暴力”接管一切;华为则通过 A2A(Agent to Agent)策略,试图在后台构建一套统一的代理调度机制。无论路线如何,2026 年对于普通消费者来说都标志着一个奇点的到来:AI 不再是聊天工具,而是显式或隐式地接管了我们的数字生活。
正如那句老话:当一个技术不再被反复提及,才说明它已真正融入生活,如同血液般不可或缺。
然而,越是无感,越要警惕。当 AI 深入工作流的每一个细节,隐私将成为最昂贵的奢侈品。在追求极致自动化与效率的同时,如何选择服务商、如何平衡本地与云端模型、如何保留最后一点象征性的“隐私”,将是我们在 2026 年必须面对的命题。
2026 来了,你开始将 AI 集成到自己的工作流中了吗?
🚀 《肘子的 Swift 周报》
每周为你精选最值得关注的 Swift、SwiftUI 技术动态
- 📮 立即订阅 | weekly.fatbobman.com 获取完整内容
- 👥 加入社区 | Discord 与 2000+ 开发者交流
- 📚 深度教程 | fatbobman.com 探索 200+ 原创文章
近期推荐
独立开发者的试炼:Zipic 从 0 到 1 的产品化之路
Zipic 是我一直在高频使用的图片压缩工具,我亲眼见证了这个应用如何从一个职场工作的小需求,逐渐在作者 十里 的不断打磨下成长为一个高效、精致、专注的成功产品。独立开发者往往意味着“一人成军”,时刻在策略、设计、开发、分发与推广之间来回切换。为了挖掘这背后的故事,我邀请了十里复盘了 Zipic 从 0 到 1 的全过程。全文共分三个篇章:产品设计(本文)、不依赖 Mac App Store 的分发与售卖 以及 技术细节复盘:SwiftUI 痛点与性能瓶颈。
Swift vs. Rust:从内存管理的终极对决中学到的 5 个惊人事实
在开发者社区中,关于 Swift 和 Rust 性能的讨论从未停止。通常的看法是:Swift 因为自动引用计数(ARC)而相对较慢,而 Rust 则以其极致的速度和内存效率著称。但 Snow 认为,这种“快”与“慢”的简单标签往往掩盖了两者在设计哲学上的根本差异:Swift 优先开发体验和生态兼容,Rust 追求极致性能和编译时安全。
结合实际案例,文章揭示了五个真相:Rust 的所有权规则本质上是零开销的编译时工具;Swift 的真正性能包袱来自 Objective-C 兼容性而非 ARC 本身;ARC 的核心问题是性能的不可预测性;并发安全上 Swift 依赖运行时保护而 Rust 实现编译时保证;以及为何 Swift 无法“变成”Rust。
StoreKit 订阅实战指南 (StoreKit Subscriptions: A Practical Guide)
Mohammad Azam 基于多年 iOS 开发经验和真实案例,撰写了完整的 StoreKit 订阅实践教程。系列涵盖:变现模型选择(一次性购买、订阅、消耗型购买及混合策略)、付费墙策略对比(软/硬付费墙及订阅试用的权衡)、引导体验设计(从静态截图演进到 8 步交互式引导,让用户在付费前完成核心功能体验并建立情感投入)、以及完整的技术实现(App Store Connect 配置、StoreKit 集成、产品加载和购买流程的代码示例)。
Skip 2025 回顾与 2026 路线图 (Skip 2025 Retrospective and 2026 Roadmap)
在 2025 年,随着 Swift SDK for Android 在 swift.org 正式发布,Skip 通过 Skip Fuse 提供原生编译支持,解锁了数千个原生 Swift 包在 Android 上的使用。同时新增 NFC、Stripe、PostHog、Auth0、Socket.IO 等双平台框架。iOS 26 推出的 Liquid Glass 界面风格成为跨平台框架的试金石。Skip 因采用“完全原生”策略(iOS 上使用原生 SwiftUI,Android 上映射到 Jetpack Compose)而在第一天就自动支持新界面,无需重写或变通。在 2026 年 Skip 计划继续扩展集成框架、优化 Skip Fuse 工具链、提升性能和开发体验。
如何使用 Claude Code (How to use Claude Code)
这是一份 Khoa Pham 在高强度使用 Claude Code 数月后整理的实战指南。核心技巧包括各种不同模式的详细应用场景,尤其是如何合理使用 Extended Thinking 模式以避免浪费 Token。另外还涵盖了关键快捷键、上下文管理技巧、MCP 集成、VS Code 和 Chrome 扩展、GitHub Actions 集成、Git Worktrees 并行工作流、插件生态以及提示词最佳实践等众多内容。内容详实、具体、有针对性,并非简单的功能介绍手册。
App Store Connect API Webhook 串接|提升 iOS CI/CD 自动化效率与通知流程
苹果在 WWDC 2025 中发布了 App Store Connect API Webhook,支持构建状态、App 版本状态、TestFlight 反馈等事件的实时推送。Zhong Cheng 针对打包上传后传统 Polling 方式需等待约 20 分钟(GitHub Runner 浪费 $1.24/次)的痛点,详细介绍了如何在 CI/CD 中应用该能力,实现零等待成本;GitFlow 回 master 时机可精确对齐 App 实际发布时间;开发者权限受限时也能及时收到拒审通知。
使用 WendyOS 开发嵌入式 Linux 应用 (Setting up Embedded Linux with WendyOS)
WendyOS 是一个专为嵌入式设备设计的 Linux 发行版,用 Swift 编写,旨在将 iOS 开发的便捷性带到嵌入式领域。Joannis Orlandos 在本文中提供了完整上手教程:从安装 Homebrew 和 Wendy 工具、刷写 WendyOS 到树莓派/Jetson Orin Nano 等设备、通过 USB 连接设备、配置 WiFi、创建 Swift 项目(含 wendy.json 权限配置)到使用 VSCode 扩展进行远程调试(支持断点和状态检查)。适合想将 Swift 应用到嵌入式设备或 IoT 场景的开发者作为入门教程。
工具
Swift 并发:通俗易懂的学习指南 (Fucking Approachable Swift Concurrency)
这是一个由 Pedro Piñera 创建、基于 Matt Massicotte 的 Swift 并发理念整理的学习资源,用通俗易懂的方式解释 async/await、Task、Actor、Sendable 等核心概念。Pedro 通过 "Office Building(办公楼)" 这一场景,将 MainActor 比作前台、actor 比作部门办公室、await 比作敲门等待,帮助开发者建立直观的心智模型。 另外,还提供了一个适用于 AI 工具的 Skill.md 文件,方便开发者将上述并发实践直接嵌入到开发工作流的规则引擎中。
Dimillian's Skills - iOS/Swift 开发 AI Agent Skills 集合
Thomas Ricouard 创建的用于 iOS/Swift 开发的 Skills 仓库,包含六个专注于实际工作流的 AI Agent Skills。涵盖 App Store Changelog 生成(从 git history 自动生成发布说明)、iOS Debugger Agent(使用 XcodeBuildMCP 构建/调试 iOS 项目)、Swift Concurrency Expert(修复 Swift 6.2 并发问题)、SwiftUI Liquid Glass(实现 iOS 26+ Liquid Glass API)、SwiftUI View Refactor(重构视图结构和依赖模式)、SwiftUI Performance Audit(审查性能瓶颈并提供优化建议)等。
StoreKit Helper - SwiftUI 应用内购买封装库
由jaywcjlove开发的轻量级 StoreKit 2 封装库,专为 SwiftUI 设计,大幅简化应用内购买实现。相比直接使用 StoreKit 2 API,StoreKitHelper 减少了约 70% 的样板代码,特别适合需要快速集成应用内购买且不想处理底层复杂性的 SwiftUI 开发者。
核心特性包括:基于 @ObservableObject 的状态管理、协议驱动的类型安全产品定义、实时交易监听和自动状态更新、内置的 StoreKitHelperView 和 StoreKitHelperSelectionView UI 组件。通过 hasNotPurchased/hasPurchased 属性可以轻松控制界面显示,支持链式 API 配置购买弹窗的各种回调。
求贤
Mac OS 工程师
Photon 正在构建开源基础设施,帮助开发者将 AI Agent 带到人类已经熟悉的交互界面中,例如 iMessage、WhatsApp、电话通话、Discord、Signal 等。在此之上,我们还在打造以交互为核心的开源 Agent SDK,覆盖多段消息处理、消息线程处理、表情/回应(Tapbacks)等能力,让开发者和企业能够开发真正"像人一样"交流的 Agent。
职位要求
我们正在招聘 macOS 工程师,理想的候选人应具备以下条件:
- 对 macOS 内部机制以及系统组件之间的交互有深入理解
- 具备 macOS 系统分析与调试经验
- 熟悉 macOS 系统级 API 及底层机制
- 对探索 Apple 服务中的未知部分有好奇心
- 加分项:有 iMessage、IMAgent 或相关消息基础设施的经验
薪资待遇
我们将提供具有竞争力的薪资(工作地点:美国,支持远程办公)。此外,Photon 获得多家知名投资机构的支持。
联系方式
这是朋友创业团队 Photon 的招聘。他们在做 AI Agent 在 iMessage/WhatsApp 等平台的基础设施,是个早期项目。如果你对 macOS 底层技术和早期创业机会感兴趣,可以了解一下。
往期内容
- Swift、SwiftUI 与 SwiftData:走向成熟的 2025 - #116
- 周日小插曲 - #115
- 挖掘“沉默的专家”- #114
- 当 Android 手机『强行兼容』AirDrop - #113
💝 支持与反馈
如果本期周报对你有帮助,请:
- 👍 点赞 - 让更多开发者看到
- 💬 评论 - 分享你的看法或问题
- 🔄 转发 - 帮助同行共同成长
🚀 拓展 Swift 视野
- 📮 邮件订阅 | weekly.fatbobman.com 获取独家技术洞察
- 👥 开发者社区 | Discord 实时交流开发经验
- 📚 原创教程 | fatbobman.com 学习 Swift/SwiftUI 最佳实践
2026:当 AI 隐入工作流,你准备好了吗? - 肘子的 Swift 周报 #117
大家新年好!在过去的几年中,AI 始终占据着科技界最耀眼的 C 位。但站在 2026 年的起点回看,我发现一个显著的转折:从 2025 年末开始,人们对“万亿参数”或“榜单跑分”的狂热逐渐褪去,取而代之的是对 AI 工作流深耕细作的冷静与实战。
2026 码农漫游:AI 辅助 Swift 代码修复指南
![]()
☔️ 引子
这是一个雨夜,霓虹灯的光晕在脏兮兮的窗玻璃上晕开,像极了那个该死的 View Hierarchy 渲染不出高斯模糊的样子。
在新上海的地下避难所里,老王(Old Wang)吐出一口合成烟雾,盯着全息屏幕上不断报错的终端。作为人类反抗军里仅存的几位「精通 Apple 软件开发」的工程师之一,他负责给 AI 霸主「智核(The Core)」生成的垃圾代码擦屁股。
![]()
门被撞开了,年轻的女黑客莉亚(Liya)气喘吁吁地冲进来,手里攥着一块存满代码的神经晶片。“老王!救命!‘智核’生成的 SwiftUI 代码在 iOS 26 上又崩了!反抗军的通讯 App 根本跑不起来!”
老王冷笑一声,掐灭了烟头。“我就知道。那些被捧上神坛的 LLM(大型语言模型),不管是 Claude、Codex 还是 Gemini,写起 Python 来是把好手,但一碰到 Swift,就像是穿着溜冰鞋走钢丝——步步惊心。”
在本篇博文中,您将学到如下内容:
- ☔️ 引子
- 🤖 为什么 AI 总是在 Swift 上「鬼打墙」?
- 🎨 1. 别再用过时的调色盘了
- 📐 2. 只有切掉棱角,才能圆滑处世
- 🔄 3. 监控变化,不要缺斤少两
- 📑 4. 标签页的「指鹿为马」
- 👆 5. 别什么都用「戳一戳」
- 🧠 6. 扔掉旧时代的观察者
- ☁️ 7. 数据的陷阱
- 📉 8. 性能的隐形杀手
- 🔠 9. 字体排印的法西斯
- 🔗 10. 导航的死胡同
- 🏷️ 11. 按钮的自我修养
- 🔢 12. 数组的画蛇添足
- 📂 13. 寻找文件的捷径
- 🧭 14. 导航栈的改朝换代
- 💤 15. 睡个好觉
- 🧮 16. 格式化的艺术
- 🏗️ 17. 不要把鸡蛋放在一个篮子里
- 🖼️ 18. 渲染的新欢
- 🏋️ 19. 字重的迷惑行为
- 🚦 20. 并发的万金油(也是毒药)
- 🎭 21. 主角光环是默认的
- 📐 22. 几何的诅咒
- 尾声:数字幽灵的低语
他把晶片插入接口,全息投影在空中展开。“坐下,莉亚。今天我就给你上一课,让你看看所谓的‘人工智能’是如何在 Swift 的并发地狱和快速迭代中翻车的。”
![]()
🤖 为什么 AI 总是在 Swift 上「鬼打墙」?
老王指着屏幕上乱成一锅粥的代码说道:“这不怪它们。Swift 和 SwiftUI 的进化速度比变异病毒还快。再加上 Python 和 JavaScript 的训练数据浩如烟海,而 Swift 的高质量语料相对较少,AI 常常会产生幻觉。更别提 Swift 的 Concurrency(并发) 模型,连人类专家都头秃,更别说这些只会概率预测的傻大个了。”
![]()
“听着,莉亚,”老王严肃地说,“要想在 iOS 18 甚至更高版本的废土上生存,你必须学会识别这些‘智障操作’。我们不谈哲学,只谈生存。以下就是我从死人堆里总结出来的代码排雷指南。”
🎨 1. 别再用过时的调色盘了
💀 AI 的烂代码: foregroundColor()
✨ 老王的修正: foregroundStyle()
“看这里,”老王指着一行代码,“AI 还在用 foregroundColor()。这就像是还在用黑火药做炸弹。虽然字数一样,但前者已经是个行将就木的Deprecated API。把它换成 foregroundStyle()!后者才是未来,它支持渐变(Gradients)等高级特性。别让你的 UI 看起来像上个世纪的产物。”
![]()
📐 2. 只有切掉棱角,才能圆滑处世
💀 AI 的烂代码: cornerRadius()
✨ 老王的修正: clipShape(.rect(cornerRadius:))
“又是一个老古董。cornerRadius() 早就该进博物馆了。现在的标准是使用 clipShape(.rect(cornerRadius:))。为什么?因为前者是傻瓜式圆角,后者能让你通过 uneven rounded rectangles(不规则圆角矩形)玩出花来。在这个看脸的世界,细节决定成败。”
🔄 3. 监控变化,不要缺斤少两
💀 AI 的烂代码: onChange(of: value) { ... } (单参数版本)
✨ 老王的修正: onChange(of: value) { oldValue, newValue in ... }
老王皱起眉头:“这个 onChange 修改器,AI 经常只给一个参数闭包。这在旧版本是‘不安全’的,现在已经被标记为弃用。要么不传参,要么接受两个参数(新旧值)。别搞得不清不楚的,容易出人命。”
![]()
📑 4. 标签页的「指鹿为马」
💀 AI 的烂代码: tabItem()
✨ 老王的修正: 新的 Tab API
“如果看到老旧的 tabItem(),立刻把它换成新的 Tab API。这不仅仅是为了所谓的‘类型安全(Type-safe)’,更是为了适配未来——比如那个传闻中的 iOS 26 搜索标签页设计。我们要领先‘智核’一步,懂吗?”
👆 5. 别什么都用「戳一戳」
💀 AI 的烂代码: 滥用 onTapGesture()
✨ 老王的修正: 使用真正的 Button
“AI 似乎觉得万物皆可 onTapGesture()。大错特错!除非你需要知道点击的具体坐标或者点击次数,否则统统给我换成标准的 Button。这不仅是为了让 VoiceOver(旁白)用户能活下去,也是为了让 visionOS 上的眼球追踪能正常工作。别做一个对残障人士不友好的混蛋。”
🧠 6. 扔掉旧时代的观察者
💀 AI 的烂代码: ObservableObject
✨ 老王的修正: @Observable 宏
“莉亚,看着我的眼睛。除非你对 Combine 框架有什么特殊的各种癖好,否则把所有的 ObservableObject 都扔进焚化炉,换成 @Observable 宏。代码更少,速度更快,这就好比从燃油车换成了核动力战车。”
![]()
☁️ 7. 数据的陷阱
💀 AI 的烂代码: SwiftData 模型中的 @Attribute(.unique)
✨ 老王的修正: 小心使用!
“这是一个隐蔽的雷区。如果在 SwiftData 模型定义里看到 @Attribute(.unique),你要警惕——这玩意儿跟 CloudKit 八字不合。别到时候数据同步失败,你还在那儿傻乎乎地查网络连接。”
📉 8. 性能的隐形杀手
💀 AI 的烂代码: 将视图拆分为「计算属性(Computed Properties)」 ✨ 老王的修正: 拆分为独立的 SwiftUI Views
“为了图省事,AI 喜欢把大段的 UI 代码塞进计算属性里。这是尸位素餐!尤其是在使用 @Observable 时,计算属性无法享受智能视图失效(View Invalidation)的优化。把它们拆分成独立的 SwiftUI 结构体!虽然麻烦点,但为了那 60fps 的流畅度,值得。”
🔠 9. 字体排印的法西斯
💀 AI 的烂代码: .font(.system(size: 14))
✨ 老王的修正: Dynamic Type (动态字体)
“有些 LLM(尤其是那个叫 Claude 的家伙)简直就是字体界的独裁者,总喜欢强行指定 .font(.system(size: ...))。给我搜出这些毒瘤,全部换成 Dynamic Type。如果是 iOS 26+,你可以用 .font(.body.scaled(by: 1.5))。记住,用户可能眼花,别让他们看瞎了。”
![]()
🔗 10. 导航的死胡同
💀 AI 的烂代码: 列表里的内联 NavigationLink
✨ 老王的修正: navigationDestination(for:)
“在 List 里直接写 NavigationLink 的目标地址?那是原始人的做法。现在的文明人使用 navigationDestination(for:)。解耦!解耦懂不懂?别把地图画在脚底板上。”
老王喝了一口已经凉透的咖啡,继续在这堆赛博垃圾中挖掘。
🏷️ 11. 按钮的自我修养
💀 AI 的烂代码: 用 Label 做按钮内容
✨ 老王的修正: 内联 API Button("Title", systemImage: "plus", action: ...)
“期待看到 AI 用 Label 甚至纯 Image 来做按钮内容吧——这对 VoiceOver 用户来说简直是灾难。用新的内联 API:Button("Tap me", systemImage: "plus", action: whatever)。简单,粗暴,有效。”
🔢 12. 数组的画蛇添足
💀 AI 的烂代码: ForEach(Array(x.enumerated()), ...)
✨ 老王的修正: ForEach(x.enumerated(), ...)
“看到这个 Array(x.enumerated()) 了吗?这就是脱裤子放屁。直接用 ForEach(x.enumerated(), ...) 就行了。省点内存吧,虽然现在的内存不值钱,但程序员的尊严值钱。”
![]()
📂 13. 寻找文件的捷径
💀 AI 的烂代码: 冗长的文件路径查找代码
✨ 老王的修正: URL.documentsDirectory
“那些又臭又长的查找 Document 目录的代码,统统删掉。换成 URL.documentsDirectory。一行代码能解决的事,绝不写十行。”
🧭 14. 导航栈的改朝换代
💀 AI 的烂代码: NavigationView
✨ 老王的修正: NavigationStack
“NavigationView 已经死了,有事烧纸。除非你要支持 iOS 15 那个上古版本,否则全部换成 NavigationStack。”
💤 15. 睡个好觉
💀 AI 的烂代码: Task.sleep(nanoseconds:)
✨ 老王的修正: Task.sleep(for: .seconds(1))
“‘智核’ 似乎很喜欢纳秒,可能它觉得自己算得快。但你要用 Task.sleep(for:),配合 .seconds(1) 这种人类能读懂的单位。别再像个僵尸一样数纳秒了。”
![]()
🧮 16. 格式化的艺术
💀 AI 的烂代码: C 风格格式化 String(format: "%.2f", ...)
✨ 老王的修正: Swift 原生格式化 .formatted()
“我知道 C 风格的字符串格式化很经典,但它不安全。把它换成 Swift 原生的 Text(abs(change), format: .number.precision(.fractionLength(2)))。虽然写起来长一点,但它像穿了防弹衣一样安全。”
🏗️ 17. 不要把鸡蛋放在一个篮子里
💀 AI 的烂代码: 单个文件塞入大量类型 ✨ 老王的修正: 拆分文件
“AI 喜欢把几十个 struct 和 class 塞进一个文件里,这简直是编译时间毁灭者。拆开它们!除非你想在编译的时候有时间去煮个满汉全席。”
🖼️ 18. 渲染的新欢
💀 AI 的烂代码: UIGraphicsImageRenderer
✨ 老王的修正: ImageRenderer
“如果你在渲染 SwiftUI 视图,别再用 UIKit 时代的 UIGraphicsImageRenderer 了。拥抱 ImageRenderer 吧,这是它的主场。”
![]()
🏋️ 19. 字重的迷惑行为
💀 AI 的烂代码: 滥用 fontWeight()
✨ 老王的修正: 区分 bold() 和 fontWeight(.bold)
“三大 AI 巨头都喜欢滥用 fontWeight()。记住,fontWeight(.bold) 和 bold() 渲染出来的结果未必一样。这就像‘微胖’和‘壮实’的区别,微妙但重要。”
🚦 20. 并发的万金油(也是毒药)
💀 AI 的烂代码: DispatchQueue.main.async
✨ 老王的修正: 现代并发模型
“一旦 AI 遇到并发问题,它就会像受惊的鸵鸟一样把头埋进 DispatchQueue.main.async 里。这是不可原谅的懒惰!那是旧时代的创可贴,现在的我们有更优雅的 Actor 模型。”
🎭 21. 主角光环是默认的
💀 AI 的烂代码: 到处加 @MainActor
✨ 老王的修正: 默认开启
“如果你在写新 App,Main Actor 隔离通常是默认开启的。不用像贴符咒一样到处贴 @MainActor。”
![]()
📐 22. 几何的诅咒
💀 AI 的烂代码: GeometryReader + 固定 Frame
✨ 老王的修正: visualEffect() 或 containerRelativeFrame()
“最后,也是最可怕的——GeometryReader。天哪,AI 对这玩意儿简直是真爱,还喜欢配合固定尺寸的 Frame 使用。这是布局界的核武器,一炸毁所有。试着用 visualEffect() 或者 containerRelativeFrame() 来代替。别做那个破坏布局流的罪人。”
尾声:数字幽灵的低语
老王敲下最后一个回车键,全息屏幕上的红色报错瞬间变成了令人愉悦的绿色构建成功提示。
// Human-verified Code
// Status: Compiling... Success.
// Fixed by: The Refiners (Old Wang & Liya)
“搞定。” 老王瘫坐在椅子上,听着窗外雨声渐大。
![]()
莉亚看着完美运行的 App,眼中闪烁着崇拜的光芒:“老王,你简直是神!既然我们能修复这些代码,为什么 AI 还是会不断地生成这种垃圾?”
老王点燃了最后一支烟,看着烟雾在霓虹灯下缭绕。“因为 AI 会产生幻觉(Hallucinations)。它们会编造出看起来很美、名字很像样,但实际上根本不存在的 API。这就像是在数字世界里见鬼了一样。”
![]()
他转过头,意味深长地看着莉亚:“对此,我也无能为力。我只能修补已知的错误,却无法预测未知的疯狂。”
“那么,”老王把目光投向了屏幕前的你——第四面墙之外的观察者,“轮到你了。在你的赛博探险中,通常会在 AI 生成的代码里发现什么‘惊喜’?”
![]()
如果你还活着,请在评论区告诉我们。毕竟,在这场人机大战中,知识是我们唯一的武器。
那么,感谢观赏,再会啦!8-)
Swift 6.2 列传(第十五篇):王语嫣的《万剑归宗》与 InlineArray
Swift 多线程通关指南:从 GCD 回调地狱到 Task/Actor 躺赢
各位 iOS 开发者宝子们,谁还没被多线程折磨过?想当年用 GCD 的时候,回调嵌套像套娃,线程安全像走钢丝,查个数据错乱的 Bug 能熬到半夜发际线后移。直到 Swift 5.5 甩出了「并发框架」这个王炸,Task 和 Actor 闪亮登场,才让我们摆脱了 “多线程 PUA”。
今天这篇博客,咱们就用 “唠嗑式” 风格,把 Task、Actor 的原理、用法、最佳实践和避坑指南讲得明明白白,保证你看得懂、用得上,还能顺便笑出声。
一、前言:那些年我们踩过的 GCD 坑
在聊新东西之前,先扎心回顾一下 GCD 的 “罪行”:
- 回调地狱:请求接口→解析数据→更新 UI,三层嵌套下去,代码像俄罗斯套娃,后期维护看一眼就脑壳疼;
-
线程安全玄学:多个线程同时修改一个变量,时而正常时而崩溃,数据错乱的 Bug 查半天,最后发现是忘了加
dispatch_barrier; - 生命周期失控:手动创建的队列和任务,一不小心就忘记取消,导致内存泄漏或无效操作;
-
主线程判断麻烦:更新 UI 前还要写
if Thread.isMainThread,稍不注意就闪退。
直到 Swift 并发框架上线,Task(异步任务包工头)和 Actor(线程安全管理员)强强联手,才让多线程开发从 “渡劫” 变成 “躺赢”。接下来,咱们逐个拆解这两个核心玩家。
二、核心玩家 1:Task —— 异步任务的 “包工头”
1. 什么是 Task?通俗点说就是 “干活的包工头”
你可以把 Task 理解为一个包工头,你给它分配活(异步代码),它会帮你安排工人(线程)去干,还能告诉你啥时候干完(通过await等待结果)。
它的核心作用是封装异步操作,摆脱 GCD 的闭包嵌套,让异步代码像同步代码一样线性书写 —— 这也是 Swift 并发的核心优势:异步代码同步化。
2. Task 的核心原理:结构化 vs 非结构化(家族企业 vs 野生放养)
Task 有两种核心形态,这是理解它的关键,咱们用比喻讲清楚:
(1)结构化并发(默认 Task):家族企业,父子绑定
// 结构化Task:父任务(包工头老板)
func parentTask() async {
print("老板:我要安排个小工干活")
// 子任务(小工):继承父任务的上下文(优先级、取消状态等)
let result = await Task {
print("小工:开始干活")
await Task.sleep(1_000_000_000) // 干活1秒
return "活干完了"
}.value
print("老板:小工汇报结果:(result)")
}
核心特性(家族企业规则) :
- 父任务会等子任务干完才继续执行(老板等小工汇报);
- 子任务继承父任务的 “家底”:优先级、Actor 上下文、取消状态等;
- 父任务被取消,子任务会跟着被取消(老板跑路,小工也停工);
- 编译器会自动管理任务生命周期,不用手动操心内存泄漏。
这是 Swift 官方强烈推荐的用法,也是最安全、最省心的方式。
(2)非结构化并发(Task.detached):野生放养,自生自灭
// 非结构化Task:野生包工头,和你没关系
func wildTask() {
print("我:安排个野生包工头干活")
let task = Task.detached {
print("野生包工头:自己干自己的")
await Task.sleep(1_000_000_000)
return "野生活干完了"
}
// 想拿结果得主动等
Task {
let result = await task.value
print("我:野生包工头汇报结果:(result)")
}
}
核心特性(野生规则) :
- 不继承任何上下文(优先级、Actor 等都是默认值);
- 和创建它的线程 / 任务 “断绝关系”,父不管子,子不认父;
- 生命周期完全由你手动管理,忘记取消就可能导致内存泄漏;
- 仅适用于 “不需要依赖当前上下文,完全独立的任务”(比如后台同步日志)。
3. Task 的 3 种常用创建方式(代码示例 + 场景)
| 创建方式 | 代码示例 | 适用场景 |
|---|---|---|
| 结构化 Task(默认) | Task { await doSomething() } |
大部分业务场景(接口请求、数据处理等),依赖当前上下文 |
| 非结构化 Task | Task.detached { await doSomething() } |
独立后台任务(日志同步、缓存清理等),不依赖当前上下文 |
| 指定 Actor Task | Task { @MainActor in updateUI() } |
直接切换到指定 Actor(如 MainActor 更新 UI) |
4. Task 的小知识点(必知必会)
- 优先级:可以给 Task 指定优先级,系统会优先调度高优先级任务(比如支付>后台同步):
// 高优先级:用户主动操作
Task(priority: .userInitiated) {
await processPayment()
}
// 低优先级:后台辅助操作
Task(priority: .utility) {
await syncLocalCache()
}
- 取消:Task 的取消是 “协作式” 的(不是强制枪毙,是提醒任务自己停工):
let task = Task {
// 干活前先检查是否被取消
if Task.isCancelled {
return
}
await doSomething()
// 干活中途也可以检查
try Task.checkCancellation()
await doSomethingElse()
}
// 手动取消任务
task.cancel()
-
等待结果:用
await task.value可以获取 Task 的执行结果,结构化 Task 也可以直接内联等待。
三、核心玩家 2:Actor —— 线程安全的 “卫生间管理员”
1. 线程安全的痛点:多个人抢卫生间的噩梦
先想一个场景:你和同事们共用一个卫生间(共享变量),如果没有管理员,大家同时挤进去,场面会极度混乱(数据错乱、崩溃)。
在多线程中,这个 “卫生间” 就是共享变量(比如var userList: [User]),“抢卫生间” 就是多个线程同时读写这个变量,这也是 GCD 中最头疼的问题。
2. 什么是 Actor?通俗点说就是 “卫生间管理员”
Actor 的核心作用是保证线程安全,它就像一个严格的卫生间管理员,遵守一个铁律:一次只允许一个线程(人)进入 Actor 的 “私人空间”(内部属性和方法) 。
这样一来,就从根本上杜绝了 “多线程同时读写共享变量” 的问题,不用再手动加锁、加屏障,编译器会帮你搞定一切。
3. Actor 的核心原理:隔离域 + 消息传递
Actor 的底层原理其实很简单,就两个关键点,咱们用大白话解释:
(1)隔离域(私人空间)
每个 Actor 都有自己的 “隔离域”,相当于卫生间的围墙,外部线程无法直接访问 Actor 内部的属性和方法,只能通过管理员(Actor)传递消息。
比如你不能直接写actor.userList = [],编译器会直接报错 —— 这就像你不能直接踹开卫生间门,只能跟管理员说 “我要进去”。
(2)消息传递(排队叫号)
外部线程想要操作 Actor 的内部资源,需要给 Actor 发送 “消息”(调用 Actor 的方法),Actor 会把这些消息排成一个队列,然后串行处理(一个接一个,不插队)。
这就像你跟管理员说 “我要进去”,管理员会把你排到队尾,等前面的人出来,再让你进去,完美保证了安全。
4. Actor 的使用方法(代码示例 + 场景)
(1)自定义 Actor:创建你的 “卫生间管理员”
// 定义一个Actor:用户列表管理员
actor UserManager {
// 内部共享变量(卫生间):外部无法直接访问
private var userList: [String] = []
// 提供方法(叫号服务):外部可以通过await调用
func addUser(_ name: String) {
// 这里的代码串行执行,绝对线程安全
userList.append(name)
print("添加用户:(name),当前列表:(userList)")
}
func getUserList() -> [String] {
return userList
}
}
// 使用Actor
func useUserManager() async {
// 创建Actor实例
let manager = UserManager()
// 调用Actor方法:必须加await(等管理员叫号)
await manager.addUser("张三")
await manager.addUser("李四")
// 获取用户列表
let list = await manager.getUserList()
print("最终用户列表:(list)")
}
关键注意点:调用 Actor 的任何方法都必须加await,因为 Actor 处理消息需要时间,这是一个异步操作。
(2)MainActor:专属主线程的 “UI 管理员”
除了自定义 Actor,Swift 还提供了一个特殊的 Actor——MainActor,它专门绑定主线程,是更新 UI 的 “专属通道”。
我们知道,UI 操作必须在主线程执行,以前用 GCD 要写dispatch_async(dispatch_get_main_queue()),现在用MainActor更简单:
// 方式1:修饰函数,整个函数在主线程执行
@MainActor
func updateUserName(_ name: String) {
// 这里的代码一定在主线程执行,放心更新UI
self.userNameLabel.text = name
}
// 方式2:修饰属性,属性的读写都在主线程
@MainActor var userAvatar: UIImage?
// 方式3:在Task中指定MainActor
Task { @MainActor in
self.userNameLabel.text = "张三"
}
// 方式4:await MainActor.run 局部切换主线程
Task {
// 后台执行耗时操作
let user = await fetchUser()
// 切换到主线程更新UI
await MainActor.run {
self.userNameLabel.text = user.name
}
}
MainActor 是 UI 更新的首选,不用再手动判断主线程,编译器会帮你保证 UI 操作在主线程执行,杜绝闪退。
5. Actor 的小知识点(必知必会)
- Actor 重入:Actor 允许 “嵌套调用”,比如 Actor 的方法 A 调用了方法 B,这是允许的,且仍然串行执行;
-
Actor 间通信:多个 Actor 之间调用方法,同样需要加
await,编译器会自动处理消息传递; -
不可变属性:Actor 的不可变属性(
let)可以直接访问(不用await),因为不可变属性不会有线程安全问题。
四、黄金搭档:Task + Actor 实战演练
光说不练假把式,咱们结合实际业务场景,看看 Task 和 Actor 怎么配合使用:
场景:接口请求 + 数据解析 + UI 更新(线程安全版)
// 1. 定义数据存储Actor(保证线程安全)
actor DataStore {
private var userData: UserModel?
func saveUser(_ user: UserModel) {
userData = user
}
func getUser() -> UserModel? {
return userData
}
}
// 2. 接口请求函数(后台执行)
func fetchUserFromAPI() async throws -> UserModel {
// 模拟接口请求(后台线程)
await Task.sleep(1_000_000_000)
return UserModel(name: "李四", age: 25)
}
// 3. 核心业务逻辑(Task + Actor + MainActor)
func loadUserData() {
// 结构化Task:管理异步流程
Task {
do {
// 步骤1:主线程显示加载动画
await MainActor.run {
self.loadingView.isHidden = false
}
// 步骤2:后台请求接口(非主线程,不卡顿UI)
let user = try await fetchUserFromAPI()
// 步骤3:线程安全存储数据
let dataStore = DataStore()
await dataStore.saveUser(user)
// 步骤4:主线程更新UI + 隐藏加载动画
await MainActor.run {
self.userNameLabel.text = user.name
self.ageLabel.text = "(user.age)"
self.loadingView.isHidden = true
}
} catch {
// 异常处理:主线程隐藏加载动画 + 提示错误
await MainActor.run {
self.loadingView.isHidden = true
self.toastLabel.text = "请求失败:(error.localizedDescription)"
}
}
}
}
这个示例完美结合了 Task(异步流程管理)、Actor(数据存储线程安全)、MainActor(UI 更新),没有回调嵌套,线程安全有保障,UI 不卡顿,这就是 Swift 并发的正确打开方式!
五、最佳实践:少踩坑,多摸鱼
掌握了原理和用法,接下来的最佳实践能让你在实际开发中事半功倍,少走弯路:
1. 优先使用结构化 Task,拒绝放养式 Task.detached
结构化 Task 的生命周期由编译器管理,安全省心,90% 的场景都用它。只有在需要完全独立的后台任务(如日志同步)时,才考虑 Task.detached,且一定要手动管理取消。
2. UI 更新认准 MainActor,别在后台瞎折腾
无论用@MainActor修饰函数、还是await MainActor.run,都要保证 UI 操作在主线程执行,这是杜绝 UI 闪退和卡顿的关键。
3. Actor 里只放线程不安全的状态,别啥都往里塞
Actor 的方法是串行执行的,如果把非共享的、不需要线程安全的逻辑也放进 Actor,会降低执行效率。Actor 只负责管理 “共享可变状态”(如用户列表、缓存数据)。
4. 用 TaskGroup 管理多任务,批量控制更省心
如果需要并行执行多个任务(如批量请求接口),用TaskGroup比手动创建多个 Task 更方便,支持批量添加、批量取消、批量获取结果:
await withTaskGroup(of: UserModel.self) { group in
// 批量添加任务
for userId in [1,2,3] {
group.addTask {
return await fetchUserById(userId)
}
}
// 批量获取结果
for await user in group {
print("获取到用户:(user.name)")
}
}
5. defer 里别乱创 Task,小心 “幽灵任务”
这是咱们之前踩过的坑:defer块里创建的异步 Task,可能因为上下文销毁而无法执行(比如页面关闭后,Task 还没被调度),导致加载动画关不掉、资源清理不彻底。
6. 关键节点检查 Task 取消状态,避免无效操作
如果用户中途退出页面,对应的 Task 应该被取消,在耗时操作前后检查Task.isCancelled或try Task.checkCancellation(),可以及时终止无效操作,节省资源。
六、避坑指南:那些让你头秃的坑
即使掌握了最佳实践,也难免踩坑,这些坑你一定要警惕:
1. 坑 1:Actor 重入 —— 看似串行,实则可能嵌套执行
Actor 允许方法嵌套调用,比如:
actor MyActor {
func methodA() async {
print("A开始")
await methodB()
print("A结束")
}
func methodB() async {
print("B执行")
}
}
调用await myActor.methodA()时,会输出 “A 开始→B 执行→A 结束”,这是正常的,且仍然线程安全,不用过度担心。
2. 坑 2:Task 取消是 “协作式”,不是 “强制枪毙”
Task 不会被强制终止,只有在 “取消检查点” 才会响应取消:
- ✅ 取消检查点:
await异步操作、try Task.checkCancellation()、await Task.yield() - ❌ 非检查点:长时间同步循环(如
for i in 0..<1000000),不会响应取消
如果有长时间同步代码,要手动插入取消检查:
Task {
for i in 0..<1000000 {
// 手动检查取消状态
if Task.isCancelled {
return
}
heavySyncWork(i)
}
}
3. 坑 3:在 MainActor 函数里执行耗时操作,导致 UI 卡顿
@MainActor修饰的函数会在主线程执行,如果在里面执行耗时操作(如大数据解析、复杂加密),会阻塞主线程,导致 UI 卡顿:
// ❌ 错误做法:主线程执行耗时解析
@MainActor
func parseLargeData(_ data: Data) {
let model = try! JSONDecoder().decode(LargeModel.self, from: data)
self.model = model
}
// ✅ 正确做法:后台解析,主线程更新UI
func loadLargeData() {
Task {
// 后台解析
let model = await Task.detached {
return try! JSONDecoder().decode(LargeModel.self, from: data)
}.value
// 主线程更新UI
await MainActor.run {
self.model = model
}
}
}
4. 坑 4:直接访问 Actor 的属性,编译器会报错
Actor 的属性是隔离的,外部无法直接访问,必须通过方法获取:
// ❌ 错误做法:直接访问Actor属性
let manager = UserManager()
print(manager.userList) // 编译器报错
// ✅ 正确做法:通过Actor方法获取
let list = await manager.getUserList()
print(list)
5. 坑 5:非结构化 Task 忘记取消,导致内存泄漏
Task.detached 创建的任务如果持有了self,且忘记取消,会导致self无法释放,内存泄漏:
// ❌ 错误做法:忘记取消Task
func badTask() {
Task.detached { [weak self] in
guard let self = self else { return }
while true {
await self.syncLog()
await Task.sleep(10_000_000_000)
}
}
}
// ✅ 正确做法:手动持有Task,在合适时机取消
class MyVC: UIViewController {
private var syncTask: Task<Void, Never>?
override func viewDidLoad() {
super.viewDidLoad()
syncTask = Task.detached { [weak self] in
guard let self = self else { return }
while !Task.isCancelled {
await self.syncLog()
await Task.sleep(10_000_000_000)
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 页面消失时取消任务
syncTask?.cancel()
}
}
七、总结:Swift 多线程的正确打开方式
- 告别 GCD 回调地狱:用 Task 把异步代码写成同步风格,线性书写,易读易维护;
- 告别线程安全玄学:用 Actor(尤其是 MainActor)保证线程安全,不用手动加锁;
- 优先结构化并发:90% 的场景用默认 Task,少用 Task.detached,避免生命周期失控;
-
UI 更新认准 MainActor:无论是
@MainActor还是await MainActor.run,保证 UI 在主线程执行; - 关键节点检查取消:在耗时操作前后检查 Task 取消状态,避免无效操作;
- 用 TaskGroup 管理多任务:批量添加、批量取消,效率更高。
Swift 的 Task 和 Actor 不是银弹,但它们确实让多线程开发变得更简单、更安全。从 GCD 过渡到 Swift 并发框架,可能需要一点时间,但一旦掌握,你会发现打开了新世界的大门 —— 原来多线程开发也可以这么轻松!
最后,送大家一句话:多线程不可怕,只要用好 Task 和 Actor,你也能躺赢!
同步的 defer,异步的陷阱:Swift 并发中加载动画关不掉的调试实录
在 Swift 并发编程中,defer语句与Task的组合常常暗藏认知偏差,很容易写出 “看似合理、实际失效” 的代码。本文将通过一次真实的调试经历,拆解 “为什么defer中的代码看似合理却没有执行” 的核心原因,并梳理对应的最佳实践与避坑指南。
场景重现:挥之不去的支付加载动画
在支付页面的开发中,我们需要实现一个基础功能:支付流程执行完毕后,自动关闭加载动画。最初的代码实现如下,逻辑看似无懈可击,但实际运行中,加载动画偶尔会 “幽灵般” 无法关闭。
func processPayment() {
Task {
showLoading = true
defer {
// 主观预期:此处代码会可靠执行,关闭加载动画
Task { @MainActor in
showLoading = false
}
}
let result = await paymentService.pay()
handleResult(result)
}
}
核心知识点拆解:问题的本质
知识点 1:defer的执行边界 —— 仅保证同步代码可靠执行
defer语句的核心特性是在当前作用域退出时必然执行,无论作用域是正常返回、抛出错误还是被取消。但这一 “必然执行” 的保证,仅针对defer块内的同步代码。
func example() {
defer {
print("1. 我一定会执行(同步代码)")
Task {
print("2. 我可能不会执行(异步任务)")
}
}
print("3. 正常业务代码")
}
上述代码中,print("1. 我一定会执行")会百分百触发,但内部创建的异步Task可能还未被系统调度,当前作用域就已完全销毁,导致异步任务无法执行。
知识点 2:Swift Task的取消特性 —— 协作式而非强制式
Swift 的Task取消遵循 “协作式” 原则,而非强制终止任务运行。这一特性决定了defer本身的执行稳定性,但无法保障defer内新创建异步任务的执行。
Task {
defer {
print("即使任务被取消,我也会执行")
}
// 此处会自动检查任务取消状态
try await someAsyncWork()
// 若任务被取消,上面的await会抛出CancellationError
// 但defer块仍会不受影响地执行
}
关键痛点:defer块本身会可靠执行,但其中新创建的异步任务,可能因调度延迟、上下文销毁等问题,无法正常执行后续逻辑。
知识点 3:页面销毁时的 “时间差”—— 状态失效的隐形杀手
当支付流程完成后执行页面销毁操作时,时序上的错位会直接导致加载动画关闭逻辑失效,这也是问题复现的核心场景。
问题时序线
-
await paymentService.pay()执行完成,dismissPage()被调用,页面开始销毁流程 - SwiftUI 框架开始销毁当前 View 实例,释放相关资源
- View 中的
@State(showLoading)等状态变量被清理失效 - 外层
Task作用域退出,defer块执行,创建新的异步Task - 新
Task尚未被系统调度,View 已完全销毁 - 即便后续新
Task被调度执行,showLoading = false对已销毁的 View 无任何效果,动画无法关闭
正确解决方案:抛弃 “嵌套异步”,直接主线程同步执行
解决该问题的核心思路是:避免在defer中创建新异步任务,直接通过await MainActor.run在主线程同步执行 UI 更新操作,消除调度延迟与上下文失效的风险。
func processPayment() {
Task {
// 主线程开启加载动画
await MainActor.run {
showLoading = true
}
let result = await paymentService.pay()
// ✅ 最优解:主线程同步执行,确保逻辑可靠触发
await MainActor.run {
showLoading = false
handleResult(result)
}
}
}
该方案的优势
-
await MainActor.run会阻塞当前Task,等待主线程上的 UI 操作执行完成后再继续,无调度延迟 - 不创建新的异步
Task,直接复用外层Task上下文,避免上下文销毁导致的逻辑失效 - 即使外层
Task被取消,await之前的代码已执行完毕,await内的逻辑也会优先完成核心清理工作
延伸知识点:Swift Task 生命周期深度解析
1. Task 的三种核心创建方式
| 创建方式 | 特性 | 适用场景 |
|---|---|---|
结构化并发(推荐)Task { /* 代码 */ }
|
继承当前上下文(Actor、优先级、取消状态等) | 大部分业务场景,依赖当前上下文的异步操作 |
非结构化并发Task.detached { /* 代码 */ }
|
拥有独立执行上下文,不继承当前环境 | 无需依赖当前上下文的独立异步任务 |
指定 Actor 执行Task { @MainActor in /* 代码 */ }
|
绑定指定 Actor(如主线程)执行,自动处理线程切换 | 直接更新 UI 或操作 Actor 内状态的场景 |
2. Task 的取消检查点
Task仅在特定时机自动检查取消状态,非检查点内的长时间同步代码会无视取消指令,导致任务 “无法终止”。
Task {
// ✅ 自动检查取消状态的时机
try await someAsyncOperation() // 异步等待时自动检查
try Task.checkCancellation() // 手动主动检查取消状态
await Task.yield() // 让出执行权时自动检查
// ❌ 不检查取消状态的场景
for i in 0..<1000000 {
// 长时间同步循环,不会响应取消指令
heavySyncWork(i)
}
}
3. 多任务管理:TaskGroup 的使用
当需要并行执行多个异步任务并统一管理时,TaskGroup是最优选择,可实现批量任务添加、结果汇总、批量取消等功能。
await withTaskGroup(of: Result.self) { group in
// 批量添加任务
for item in items {
group.addTask {
await processItem(item)
}
}
// 按需批量取消所有任务(如某个任务失败时)
// group.cancelAll()
// 遍历获取所有任务结果
for await result in group {
handleTaskResult(result)
}
}
最佳实践总结
✅ 推荐做法
- UI 更新优先使用
await MainActor.run,同步执行确保逻辑可靠 - 坚决避免在
defer块中创建新的异步Task,规避调度与上下文风险 - 优先采用结构化并发(默认
Task)管理任务生命周期,简化上下文继承 - 在长时间异步流程中,主动添加取消检查点(
try Task.checkCancellation()) - 多任务并行场景,使用
TaskGroup实现统一管理与批量控制
// 标准优雅的代码示例
Task {
// 第一步:主线程更新UI(开启加载/更新状态)
await MainActor.run {
updateUI()
}
// 第二步:执行核心异步业务逻辑
let result = await processData()
// 第三步:主线程同步更新结果/关闭加载
await MainActor.run {
showResult(result)
}
}
❌ 避免做法
- 在
defer中创建异步Task执行清理或 UI 更新操作 - 主观假设异步任务会被 “立即调度执行”
- 忽略
Task的取消状态,导致长时间任务无法终止 - 滥用
Task.detached(非结构化并发),增加上下文管理成本 - 直接在非主线程
Task中修改@State等 UI 相关状态
// ❌ 需坚决规避的不良代码
defer {
Task { @MainActor in
cleanup() // 可能因调度延迟或上下文销毁而无法执行
}
}
实用调试技巧
1. 日志追踪:明确代码执行时序
通过添加有序日志,可快速定位defer与Task的执行顺序,排查是否存在异步任务未执行的问题。
Task {
print("1. 外层Task开始执行")
defer {
print("2. defer块开始执行")
}
await MainActor.run {
print("3. MainActor.run内UI操作执行")
}
print("4. 外层Task即将结束")
}
2. 主动检查:确认 Task 取消状态
在关键业务节点主动检查任务取消状态,可提前终止无效逻辑,避免资源浪费。
Task {
// 关键节点检查取消状态
if Task.isCancelled {
print("任务已被取消,终止后续操作")
return
}
// 继续执行核心业务逻辑
let result = await processBusiness()
}
3. 优先级控制:确保关键任务优先执行
通过指定Task优先级,可让核心业务(如支付结果处理、加载动画关闭)优先被系统调度,减少执行延迟。
// 高优先级:用户主动触发的核心操作
Task(priority: .userInitiated) {
await processPayment()
}
// 低优先级:后台无关紧要的辅助操作
Task(priority: .utility) {
await syncLocalData()
}
结语:让 Swift 并发代码更可靠
Swift 并发编程的核心难点,在于理解同步操作与异步操作的执行边界,以及Task的生命周期管理。defer语句的 “同步可靠性” 与Task的 “异步调度性” 形成的反差,是导致加载动画无法关闭的根本原因。
在实际开发中,只要遵循 “避免defer内嵌套异步任务”“优先使用await MainActor.run更新 UI”“采用结构化并发管理任务” 的原则,就能有效避开这类隐形陷阱,让代码从 “应该会工作” 变成 “必然会工作”,构建更稳定、更可靠的并发逻辑。
Swift 6.2 列传(第十四篇):岳灵珊的寻人启事与 Task Naming
![]()
摘要:在成千上万个并发任务的洪流中,如何精准定位那个“负心”的 Bug?Swift 6.2 带来的
Task Naming就像是给每个游荡的灵魂挂上了一个“身份铭牌”。本文将借大熊猫侯佩与岳灵珊在赛博华山的奇遇,为您解析 SE-0469 的奥秘。
0️⃣ 🐼 序章:赛博华山的“无名”孤魂
赛博华山,思过崖服务器节点。
这里的云雾不是水汽,而是液氮冷却系统泄漏的白烟。大熊猫侯佩正坐在一块全息投影的岩石上,手里捧着一盒“紫霞神功”牌自热竹笋火锅,吃得津津有味。
“味道不错,就是有点烫嘴……”侯佩吹了吹热气,习惯性地摸了摸头顶——那里毛发浓密,绝对没有秃,这让他感到无比安心。作为一名经常迷路的路痴,他刚才本来想去峨眉山看妹子,结果导航漂移,不知怎么就溜达到华山来了。
![]()
忽然,一阵凄婉的哭声从代码堆栈的深处传来。
“平之……平之……你在哪条线程里啊?我找不到你……”
侯佩定睛一看,只见一位身着碧绿衫子的少女,正对着满屏滚动的 Log 日志垂泪。她容貌清丽,却神色凄苦,正是华山派掌门岳不群之女,岳灵珊。
“岳姑娘?”侯佩擦了擦嘴角的红油,“你在这哭什么?林平之那小子又跑路了?”
岳灵珊抬起泪眼,指着屏幕上密密麻麻的 Task 列表:“侯大哥,我写了一万个并发任务去搜索‘辟邪剑谱’的下落。刚才有一个任务抛出了异常(Error),但我不知道是哪一个!它们全都长得一模一样,都是匿名的 Task,就像是一万个没有脸的人……我找不到我的平之了!”
![]()
侯佩凑过去一看,果然,调试器里的任务全是 Unspecified,根本分不清谁是谁。
在本次大冒险中,您将学到如下内容:
- 0️⃣ 🐼 序章:赛博华山的“无名”孤魂
- 1️⃣ 🏷️ 拒绝匿名:给任务一张身份证
- 简单的起名艺术
- 2️⃣ 🗞️ 实战演练:江湖小报的并发采集
- 3️⃣ 💔 岳灵珊的顿悟
- 4️⃣ 🐼 熊猫的哲学时刻
- 5️⃣ 🛑 尾声:竹笋的收纳难题
“唉,”侯佩叹了口气,颇为同情,“这就是‘匿名并发’的痛啊。出了事,想找个背锅的都找不到。不过,Swift 6.2 给了我们一招‘实名制’剑法,正好能解你的相思之苦。”
这便是 SE-0469: Task Naming。
![]()
1️⃣ 🏷️ 拒绝匿名:给任务一张身份证
![]()
在 Swift 6.2 之前,创建 Task 就像是华山派招收了一批蒙面弟子,干活的时候挺卖力,但一旦有人偷懒或者走火入魔(Crash/Hang),你根本不知道是谁干的。
岳灵珊擦干眼泪:“你是说,我可以给平之……哦不,给任务起名字?”
“没错!”侯佩打了个响指,“SE-0469 允许我们在创建任务时,通过 name 参数给它挂个牌。无论是调试还是日志记录,都能直接看到名字。”
![]()
这套 API 非常简单直观:当使用 Task.init()、Task.detached() 创建新任务,或者在任务组中使用 addTask() 时,都可以传入一个字符串作为名字。
简单的起名艺术
侯佩当即在全息屏上演示了一段代码:
// 以前我们只能盲人摸象
// 现在,我们可以给任务赐名!
let task = Task(name: "寻找林平之专用任务") {
// 在任务内部,我们可以读取当前的名字
// 如果没有名字,就是 "Unknown"(无名氏)
print("当前运行的任务是: \(Task.name ?? "Unknown")")
// 假装在干活
try? await Task.sleep(for: .seconds(1))
}
![]()
“看,”侯佩指着控制台,“现在它不再是冷冰冰的内存地址,而是一个有血有肉、有名字的‘寻找林平之专用任务’了。”
2️⃣ 🗞️ 实战演练:江湖小报的并发采集
“光有个名字有什么用?”岳灵珊还是有点愁眉不展,“我有那么多个任务在跑,万一出错的是第 9527 号呢?”
“问得好!”侯佩咬了一口竹笋,摆出一副高深莫测的样子(虽然嘴角还挂着笋渣),“这名字不仅可以硬编码,还支持字符串插值!这在处理批量任务时简直是神技。”
![]()
假设我们需要构建一个结构体来通过网络加载江湖新闻:
struct NewsStory: Decodable, Identifiable {
let id: Int
let title: String // 比如 "令狐冲因酗酒被罚款"
let strap: String
let url: URL
}
现在,我们使用 TaskGroup 派出多名探子(子任务)去打探消息。如果有探子回报失败,我们需要立刻知道是哪一路探子出了问题。
let stories = await withTaskGroup { group in
for i in 1...5 {
// 关键点来了!👇
// 我们在添加任务时,动态地给它生成了名字: "Stories 1", "Stories 2"...
// 这就像是岳不群给弟子们排辈分,一目了然。
group.addTask(name: "江湖快报分队-\(i)") {
do {
let url = URL(string: "https://hws.dev/news-\(i).json")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([NewsStory].self, from: data)
} catch {
// 🚨 出事了!
// 这里我们可以直接打印出 Task.name
// 输出示例:"Loading 江湖快报分队-3 failed."
// 岳灵珊瞬间就能知道是第 3 分队被青城派截杀了!
print("加载失败,肇事者是: \(Task.name ?? "Unknown")")
return []
}
}
}
var allStories = [NewsStory]()
// 收集情报
for await stories in group {
allStories.append(contentsOf: stories)
}
// 按 ID 排序,保持队形
return allStories.sorted { $0.id > $1.id }
}
print(stories)
3️⃣ 💔 岳灵珊的顿悟
看完这段代码,岳灵珊破涕为笑:“太好了!这样一来,如果‘寻找平之’的任务失败了,我就能立刻知道是哪一次尝试失败的,是在福州失败的,还是在洛阳失败的,再也不用对着虚空哭泣了。”
![]()
侯佩点点头,语重心长地说:“在并发的世界里,可见性(Visibility) 就是生命线。一个未命名的任务,就是 unpredictable(不可预测)的风险。给了它名字,就是给了它责任。如果它跑路了(Rogue Task),我们至少知道通缉令上该写谁的名字。”
岳灵珊看着屏幕上一个个清晰的任务名称,眼中闪过一丝复杂的神色:“是啊,名字很重要。可惜,有些人的名字,刻在了心上,却在江湖里丢了……”
![]()
“停停停!”侯佩赶紧打断她,生怕她又唱起那首福建山歌,“咱们是搞技术的,不兴搞伤痕文学。现在的重点是,你的 Debug 效率提升了 1000%!”
4️⃣ 🐼 熊猫的哲学时刻
侯佩站起身,拍了拍屁股上的灰尘(虽然是全息投影,但他觉得要有仪式感)。
“其实,给代码起名字和做熊一样。我叫侯佩,所以我知道我要吃竹笋,我知道我头绝对不秃,我知道我要走哪条路(虽然经常走错)。如果我只是一只‘Anonymous Panda’,那我可能早就被抓去动物园打工了。”
![]()
“善用 Task Naming,”侯佩总结道,“它不会增加运行时的负担,但在你焦头烂额修 Bug 的时候,它就是那个为你指点迷津的‘风清扬’。”
5️⃣ 🛑 尾声:竹笋的收纳难题
帮岳灵珊解决了心病,侯佩准备收拾东西离开赛博华山。他看着自己还没吃完的一大堆竹笋,陷入了沉思。
![]()
“这竹笋太多了,”侯佩嘟囔着,“用普通的 Array 装吧,太灵活,内存跳来跳去的,影响我拔刀(吃笋)的速度。用 Tuple 元组装吧,固定是固定了,但这写法也太丑了,而且还没法用下标循环访问……”
![]()
岳灵珊看着侯佩对着一堆竹笋发愁,忍不住问道:“侯大哥,你是想要一个既有元组的‘固定大小’超能力,又有数组的‘下标访问’便捷性的容器吗?”
侯佩眼睛一亮:“知我者,岳姑娘也!难道 Swift 6.2 连这个都有?”
![]()
岳灵珊微微一笑,指向了下一章的传送门:“听说下一回,有一种神奇的兵器,叫做 InlineArray,专门治愈你的‘性能强迫症’。”
![]()
(欲知后事如何,且看下回分解:InlineArray —— 当元组和数组生了个混血儿,熊猫的竹笋终于有地儿放了。)
![]()
SwiftUI 涨知识:如何按条件动态切换 Toggle 视图的样式(.button 或 .switch)
Swift——高阶函数(map、filter、reduce、forEach、sorted、contains……)
本文主要讲解 map、filter、reduce、forEach、sorted、contains 、 first(where:) / last(where:) 、firstIndex 和 lastIndex 、prefix( :) 和 dropFirst( :) 、 allSatisfy(_:) 、 lazy:延迟加载
一、map
map 函数,Swift 中最常用的高阶函数之一,核心作用是将集合中的每个元素按照指定规则转换,返回一个新的同类型集合,非常适合批量处理数组、字典等集合类型的元素。
map 就像一个 “转换器”:遍历集合中的每一个元素,把每个元素传入你定义的转换规则(闭包),然后将转换后的结果收集起来,形成一个新的集合返回。
- 原集合不会被修改(纯函数特性)
- 新集合的元素数量和原集合完全一致
- 新集合的元素类型可以和原集合不同
let prices = [100,200,300]
let discountedPrices = prices.map{$0 * 10}
print(discountedPrices) // [1000, 2000, 3000]
let cast = ["Vivien", "Marlon", "Kim", "Karl"]
let lowercaseNames = cast.map{$0.lowercased()}
print(lowercaseNames) // ["vivien", "marlon", "kim", "karl"]
let letterCounts = cast.map{$0.count}
print(letterCounts)// [6, 6, 3, 4]
二、 filter
filter 函数和 map 并列的核心高阶函数,filter的核心作用是根据指定条件筛选集合中的元素,返回符合条件的新集合,非常适合从数组、字典等集合中 “挑选” 需要的元素。
filter 就像一个 “筛选器”:遍历集合中的每一个元素,把每个元素传入你定义的判断条件(闭包),只有满足条件(闭包返回 true)的元素会被保留,最终返回一个包含所有符合条件元素的新集合。
- 原集合不会被修改
- 新集合的元素数量 ≤ 原集合
- 新集合的元素类型和原集合完全一致
// 示例1:筛选数字数组中的偶数
let numbers = [1, 2, 3, 4, 5, 6, 7, 8]
let evenNumbers = numbers.filter { number in
return number % 2 == 0
}
print(evenNumbers) // 输出:[2, 4, 6, 8]
// 简化写法
let evenNumbersShort = numbers.filter { $0 % 2 == 0 }
print(evenNumbersShort) // 输出:[2, 4, 6, 8]
// 示例2:筛选字符串数组中长度大于5的元素
let fruits = ["apple", "banana", "orange", "grape", "watermelon"]
let longFruits = fruits.filter { $0.count > 5 }
print(longFruits) // 输出:["banana", "orange", "watermelon"]
// 示例3:筛选自定义对象数组(比如筛选年龄≥18的用户)
struct User {
let name: String
let age: Int
}
let users = [
User(name: "张三", age: 17),
User(name: "李四", age: 20),
User(name: "王五", age: 25)
]
let adultUsers = users.filter { $0.age >= 18 }
print(adultUsers.map { $0.name }) // 输出:["李四", "王五"]
三、reduce
reduce 核心作用是将集合中的所有元素 “归约”/“汇总” 成一个单一的值(比如求和、拼接字符串、计算总宽度、生成字典等),可以理解为把一组元素 “压缩” 成一个结果。
reduce 就像一个 “汇总器”:从一个初始值开始,遍历集合中的每一个元素,将当前元素与累计结果做指定运算,最终得到一个单一的汇总值。
- 原集合不会被修改
- 最终结果的类型可以和集合元素类型不同(比如数组元素是
Int,结果可以是String;元素是CGFloat,结果可以是CGFloat) - 核心逻辑:
初始值 + 元素1 → 累计值1 + 元素2 → 累计值2 + ... → 最终结果
// 示例1:数字数组求和(最基础用法)
let numbers = [1, 2, 3, 4, 5]
// 初始值为0,累计规则:累计值 + 当前元素
let sum = numbers.reduce(0) { partialSum, number in
return partialSum + number
}
// 简化写法
let sumShort = numbers.reduce(0, +) // 直接用运算符简写,等价于上面的闭包
print(sum) // 输出:15
// 示例2:字符串数组合并成一个完整字符串
let words = ["Hello", " ", "Swift", " ", "reduce!"]
// 初始值为空字符串,累计规则:拼接字符串
let sentence = words.reduce("") { $0 + $1 }
print(sentence) // 输出:"Hello Swift reduce!"
// 示例3:计算数组中最大值(初始值设为最小值)
let scores = [85, 92, 78, 95, 88]
let maxScore = scores.reduce(Int.min) { max($0, $1) }
print(maxScore) // 输出:95
四、 forEach
forEach 函数,它是集合的基础遍历方法,核心作用是遍历集合中的每一个元素并执行指定操作,和传统的 for-in 循环功能类似,但写法更简洁,且是函数式编程风格的遍历方式。
forEach 就像一个 “遍历执行器”:按顺序遍历集合中的每一个元素,对每个元素执行你定义的闭包操作(比如打印、修改属性、调用方法等)。
- 原集合不会被修改(除非你在闭包内主动修改元素的可变属性)
- 没有返回值(
Void),这是和map/filter/reduce最大的区别(后三者都返回新集合 / 值) - 无法用
break/continue中断 / 跳过遍历(如需中断,建议用传统for-in循环)
// 示例1:遍历打印数组元素
let fruits = ["apple", "banana", "orange"]
fruits.forEach { fruit in
print("水果:\(fruit)")
}
// 简化写法
fruits.forEach { print("水果:\($0)") }
let numbers = [1, 2, 3, 4, 5]
// 需求:遍历到3时停止
// ❌ forEach 无法中断,会遍历所有元素
numbers.forEach {
if $0 == 3 {
return // 仅跳过当前元素,不会中断整体遍历
}
print($0) // 输出:1,2,4,5
}
// ✅ for-in 可以中断
for number in numbers {
if number == 3 {
break // 直接中断遍历
}
print(number) // 输出:1,2
}
五、 sorted 排序
sorted 函数,它是集合中用于排序的核心高阶函数,核心作用是将集合中的元素按指定规则排序,返回一个新的有序集合(原集合保持不变)。
// 示例1:数字数组默认排序(升序)
let numbers = [5, 2, 9, 1, 7]
let sortedNumbers = numbers.sorted()
print(sortedNumbers) // 输出:[1, 2, 5, 7, 9]
// 示例2:自定义降序排序
let descendingNumbers = numbers.sorted { $0 > $1 }
print(descendingNumbers) // 输出:[9, 7, 5, 2, 1]
// 示例3:字符串数组排序(默认字母序,区分大小写)
let fruits = ["banana", "Apple", "orange", "grape"]
let sortedFruits = fruits.sorted()
print(sortedFruits) // 输出:["Apple", "banana", "grape", "orange"]
// 示例4:字符串忽略大小写排序(自定义规则)
let caseInsensitiveFruits = fruits.sorted { $0.lowercased() < $1.lowercased() }
print(caseInsensitiveFruits) // 输出:["Apple", "banana", "grape", "orange"](和上面结果一样,但逻辑更通用)
六、contains
contains 函数,它是集合用于判断 “是否包含指定元素 / 符合条件的元素” 的核心方法,核心作用是快速检查集合中是否存在目标元素或满足条件的元素,返回布尔值(true/false)。
contains 就像一个 “检测器”:遍历集合并检查是否存在符合要求的元素,无需手动遍历判断,代码更简洁。
- 有两个常用变体:
-
contains(_:):检查是否包含具体某个元素(要求元素遵循Equatable协议,如 Int、String、CGSize 等默认遵循) -
contains(where:):检查是否包含符合自定义条件的元素(更灵活,适用于复杂判断)
-
- 返回值是
Bool(true= 包含,false= 不包含) - 原集合不会被修改
// 示例1:检查是否包含具体数字
let numbers = [1, 2, 3, 4, 5]
let hasThree = numbers.contains(3)
let hasTen = numbers.contains(10)
print(hasThree) // 输出:true
print(hasTen) // 输出:false
// 示例2:检查是否包含具体字符串
let fruits = ["apple", "banana", "orange"]
let hasBanana = fruits.contains("banana")
print(hasBanana) // 输出:true
// 示例3:检查是否包含符合条件的元素(数字大于3)
let hasGreaterThanThree = numbers.contains { $0 > 3 }
print(hasGreaterThanThree) // 输出:true(4、5都满足)
// 示例4:检查是否包含长度大于5的字符串
let hasLongFruit = fruits.contains { $0.count > 5 }
print(hasLongFruit) // 输出:true(banana、orange长度都大于5)
七、 first(where:) 和 last(where:)
first(where:) 和 last(where:) 方法,它们是集合中用于精准查找第一个 / 最后一个符合条件元素的核心方法,返回值是可选类型(T?)—— 找到则返回对应元素,找不到则返回 nil。
first(where:) / last(where:) 就像 “精准查找器”:
-
first(where:):从前往后遍历集合,返回第一个满足条件的元素(可选值) -
last(where:):从后往前遍历集合,返回最后一个满足条件的元素(可选值) - 两者都不会修改原集合,且找到目标元素后会立即停止遍历(性能优于先
filter再取first/last) - 若集合为空或无符合条件的元素,返回
nil
// 示例1:查找第一个大于3的数字
let numbers = [1, 2, 3, 4, 5, 4, 3]
let firstGreaterThan3 = numbers.first { $0 > 3 }
print(firstGreaterThan3) // 输出:Optional(4)(第一个满足的是索引3的4)
// 示例2:查找最后一个大于3的数字
let lastGreaterThan3 = numbers.last { $0 > 3 }
print(lastGreaterThan3) // 输出:Optional(4)(最后一个满足的是索引5的4)
// 示例3:查找第一个长度大于5的字符串
let fruits = ["apple", "banana", "orange", "grape"]
let firstLongFruit = fruits.first { $0.count > 5 }
print(firstLongFruit) // 输出:Optional("banana")
// 示例4:无符合条件元素时返回nil
let firstGreaterThan10 = numbers.first { $0 > 10 }
print(firstGreaterThan10) // 输出:nil
八、 firstIndex 和 lastIndex
firstIndex(of:) / firstIndex(where:) 和 lastIndex(of:) / lastIndex(where:) 方法,它们是集合中用于查找元素对应索引的核心方法,返回值为可选类型的 Index(通常是 Int 类型)—— 找到则返回元素的索引,找不到则返回 nil。
| 方法 | 作用 | 适用条件 |
|---|---|---|
firstIndex(of:) |
从前往后找第一个匹配指定元素的索引 | 元素遵循 Equatable 协议 |
firstIndex(where:) |
从前往后找第一个符合自定义条件的元素的索引 | 无(更灵活,支持复杂判断) |
lastIndex(of:) |
从后往前找最后一个匹配指定元素的索引 | 元素遵循 Equatable 协议 |
lastIndex(where:) |
从后往前找最后一个符合自定义条件的元素的索引 | 无 |
- 所有方法返回值都是
Index?(数组中等价于Int?),找不到则返回nil - 找到目标后立即停止遍历,性能高效
- 原集合不会被修改
// 基础数组
let numbers = [1, 2, 3, 2, 5, 2]
let fruits = ["apple", "banana", "orange", "banana"]
// 示例1:firstIndex(of:) —— 找第一个2的索引
if let firstTwoIdx = numbers.firstIndex(of: 2) {
print("第一个2的索引:\(firstTwoIdx)") // 输出:1
}
// 示例2:lastIndex(of:) —— 找最后一个2的索引
if let lastTwoIdx = numbers.lastIndex(of: 2) {
print("最后一个2的索引:\(lastTwoIdx)") // 输出:5
}
// 示例3:firstIndex(where:) —— 找第一个大于3的数字的索引
if let firstGreater3Idx = numbers.firstIndex { $0 > 3 } {
print("第一个大于3的数字索引:\(firstGreater3Idx)") // 输出:4(数字5)
}
// 示例4:lastIndex(where:) —— 找最后一个"banana"的索引
if let lastBananaIdx = fruits.lastIndex { $0 == "banana" } {
print("最后一个banana的索引:\(lastBananaIdx)") // 输出:3
}
// 示例5:无匹配元素时返回nil
if let noExistIdx = numbers.firstIndex(of: 10) {
print(noExistIdx)
} else {
print("未找到元素10") // 输出:未找到元素10
}
九、prefix( :) 和 dropFirst( :)
prefix(:) 和 dropFirst(:) 方法,它们是集合中用于截取 / 剔除前 N 个元素的核心方法,返回新的集合片段(PrefixSequence/DropFirstSequence,可直接转为数组),原集合保持不变。
| 方法 | 核心作用 | 返回值类型 | 原集合影响 |
|---|---|---|---|
prefix(_:) |
截取集合前 n 个元素(若 n 超过集合长度,返回全部元素) | PrefixSequence<T> |
无 |
dropFirst(_:) |
剔除集合前 n 个元素,返回剩余元素(若 n 超过集合长度,返回空集合) | DropFirstSequence<T> |
无 |
- 补充:还有无参数简化版
prefix()(等价于prefix(1),取第一个元素)、dropFirst()(等价于dropFirst(1),剔除第一个元素); - 返回的
Sequence可通过Array()转为普通数组,方便后续操作。
// 基础数组
let numbers = [1, 2, 3, 4, 5]
let fruits = ["apple", "banana", "orange", "grape"]
// 示例1:prefix(_:) —— 截取前3个元素
let prefix3Numbers = Array(numbers.prefix(3))
print(prefix3Numbers) // 输出:[1, 2, 3]
// 示例2:prefix(_:) —— n 超过数组长度,返回全部
let prefix10Numbers = Array(numbers.prefix(10))
print(prefix10Numbers) // 输出:[1, 2, 3, 4, 5]
// 示例3:dropFirst(_:) —— 剔除前2个元素
let drop2Numbers = Array(numbers.dropFirst(2))
print(drop2Numbers) // 输出:[3, 4, 5]
// 示例4:dropFirst(_:) —— n 超过数组长度,返回空
let drop10Numbers = Array(numbers.dropFirst(10))
print(drop10Numbers) // 输出:[]
// 示例5:无参数版
let firstFruit = Array(fruits.prefix(1)) // 等价于 prefix(1)
let restFruits = Array(fruits.dropFirst()) // 等价于 dropFirst(1)
print(firstFruit) // 输出:["apple"]
print(restFruits) // 输出:["banana", "orange", "grape"]
九、 allSatisfy(_:)
allSatisfy 方法,它是集合中用于判断所有元素是否都满足指定条件的核心方法,返回布尔值(true/false)—— 只有当集合中每一个元素都符合条件时返回 true,只要有一个不符合就返回 false。
allSatisfy 就像一个 “全量校验器”:
- 遍历集合中的每一个元素,依次检查是否符合条件;
- 只要发现一个元素不符合条件,会立即停止遍历(性能高效),返回
false; - 只有所有元素都符合条件,才会遍历完成并返回
true; - 对空集合调用
allSatisfy会直接返回true(逻辑上 “空集合中所有元素都满足条件”); - 原集合不会被修改。
// 基础数组
let numbers = [2, 4, 6, 8]
let mixedNumbers = [2, 4, 7, 8]
let fruits = ["apple", "banana", "orange"]
// 示例1:检查所有数字是否都是偶数
let allEven = numbers.allSatisfy { $0 % 2 == 0 }
print(allEven) // 输出:true
let mixedEven = mixedNumbers.allSatisfy { $0 % 2 == 0 }
print(mixedEven) // 输出:false(7是奇数,遍历到7时立即返回false)
// 示例2:检查所有字符串长度是否大于3
let allLongerThan3 = fruits.allSatisfy { $0.count > 3 }
print(allLongerThan3) // 输出:true(apple=5, banana=6, orange=6)
// 示例3:空集合调用返回true
let emptyArray: [Int] = []
let emptyAllMatch = emptyArray.allSatisfy { $0 > 10 }
print(emptyAllMatch) // 输出:true
十、 lazy:延迟加载
let hugeRange = 1...1000000
let result = hugeRange.lazy
.filter { $0 % 3 == 0 }
.map { $0 * 2 }
.prefix(10)
lazy会延迟计算,直到真正需要结果时才执行操作,避免创建大量中间数组。
HelloGitHub 第 117 期
告别 GeometryReader:SwiftUI .visualEffect 实战解析
Swift、SwiftUI 与 SwiftData:走向成熟的 2025 -- 肘子的 Swift 周报 #116
![]()
Swift、SwiftUI 与 SwiftData:走向成熟的 2025
在过去的几天里,我回顾了这一年来 Swift、SwiftUI 以及 SwiftData 的演进。总的感觉是:惊喜虽不算多,但“成熟感”却在不经意间扑面而来。
毋庸置疑,Swift 今年的重头戏在于改善并发编程的体验。尽管新增的选项和关键字在短期内又给开发者带来了不小的困扰,但经过这几个月的讨论与实践,社区已经显现出逐渐总结出新范式实践路径的趋势。我不认为新范式被确立且广泛接受会是一个简单、迅速的过程,但或许再过一两年,开发者对 Swift 的讨论重心将从并发转向跨平台,届时 Swift 也将迈入全新的发展阶段。
今年 SwiftUI 的更新重心大多集中在 Liquid Glass 的适配上。受限于系统初期的实现,显示效果起初并不尽如人意,但在 iOS 26.2 版本发布后,性能与稳定性都有了显著改善。坦率地说,对于今年 SwiftUI 没有引入更多革命性的新功能,我个人是挺高兴的。这让框架团队和开发者都能获得一点喘息之机,去进一步消化这个框架。在现阶段,解决遗留问题、优化性能与稳定性,远比一味堆砌新特性更有意义。
“变化较小”在 SwiftData 身上体现得尤为明显。但我认为 SwiftData 今年的表现尤为值得肯定,特别是许多改进与新功能都向下适配到了更早的系统版本。真希望它在三年前初次发布时,就能具备现在的状态。尽管 SwiftData 目前仍缺失一些关键功能,但对于相当比例的项目而言,它已经足以胜任。有了这个稳固的基础,其未来几年在性能与功能上的提高非常值得期待。
对于 2025 年 Swift 三件套的交出的答卷,我个人是满意的,不知你的感受如何?
这是本年度的最后一期周报,由衷感谢各位一年的陪伴与厚爱。
祝大家新年快乐,Happy Coding!
🚀 《肘子的 Swift 周报》
每周为你精选最值得关注的 Swift、SwiftUI 技术动态
- 📮 立即订阅 | weekly.fatbobman.com 获取完整内容
- 👥 加入社区 | Discord 与 2000+ 开发者交流
- 📚 深度教程 | fatbobman.com 探索 200+ 原创文章
活动
iOS Conf SG 2026
下个月(1 月 21 日 - 23 日),iOS Conf SG 将在新加坡举行。我也将前往现场,并作为嘉宾进行主题为 “Using SwiftUI as a Language” 的演讲——不仅关于代码,更是关于思维方式的转换。
如果你也在附近,或者计划前往,欢迎来现场打招呼!组委会专门为我的读者提供了优惠:Fatbobman 读者专属九折优惠链接
近期推荐
我和 CloudKit 的这八年:从开源 IceCream 到商业应用实战
我一直认为,所谓的苹果生态是由很多的硬件、软件、服务、人文、气质等综合构建起来的。在这其中,CloudKit 无疑是非常重要的一环。而且对于开发者来说,用好 CloudKit 不仅可以给用户更好的体验,也能低成本的为自己的应用带来创新。
IceCream 作者 Cai Yue 分享他与 CloudKit 八年的开发历程:从 2017 年开源 IceCream 并获得 Apple 官方认可,到将 CloudKit 应用于 Music Mate 和 Setlists 等商业项目的实战经验。文章深入探讨了 CloudKit 的核心优势、关键局限以及进阶玩法。
Swift 2025 年度总结 (What's new in Swift: December 2025 Edition)
这是一篇面向 Swift 社区的年度收官综述文章,由 Tim Sneath 和 Dave Lester 撰写,系统回顾了 2025 年 Swift 生态在语言特性、平台覆盖与社区建设方面的关键进展。
文章不仅总结了 Swift 6.2 在并发模型上通过更温和的默认策略降低使用门槛,同时继续推进 C++ 互操作与内存安全能力;更重要的是,从 Android、WASM、Windows、BSD、嵌入式到 AWS 等方向的持续投入,反复强化了一个清晰信号——Swift 已不再只是围绕 Apple 平台展开的语言。
或许你未必会认同其中的每一项变化,但在迈入第二个十年后的第一个年头里,Swift 依然交出了一份相当扎实的答卷。
关于 SwiftUI 的讨论 (My PM insisted we switch to SwiftUI for a massive legacy app rewrite. The result is exactly what you'd expect)
几天前无意间在 Reddit 上看到的帖子,作者对 PM 轻易选择 SwiftUI 有所抱怨,认为其无法胜任他们一个七年前开发的应用转换。对于这个观点我不置可否,但评论区的走向却出乎意料——绝大多数参与者都坚定地站在了 SwiftUI 的一边。
大量开发者认为:
- SwiftUI 本身已经足够成熟,问题出在实施方式上
- 应该渐进式迁移,而不是一次性重写
- 避开 SwiftUI 的弱项——比如可以保留 UIKit 导航,只迁移视图层
- 多个大型项目(10+ 年历史)已成功完成迁移
这个帖子展现了一个出乎我预料的现实:SwiftUI 在实际生产环境中的采用率比我们想象的高得多;开发者社区对 SwiftUI 的信心已经建立。在 2025 年底,“SwiftUI 难堪大任”的论调或许已经站不住脚了。
作为 SwiftUI 框架的推崇者,我既喜欢该框架,也很清楚它仍有很长的路要走。如果你仍在犹豫是否应该在 SwiftUI 上下功夫,或许可以看一下我在去年写的《几个常见的关于 SwiftUI 的误解》——这篇文章讨论的很多误解,恰好在这次 Reddit 讨论中得到了印证。
非 Sendable 优先设计 (Non-Sendable First Design)
随着 Swift 6 时代的到来,开发者逐渐养成了一种惯性:要么让类型符合 Sendable,要么给它套上 @MainActor 或 actor。在这篇文章中,Matt Massicotte 提出了一个极具启发性的哲学:“非 Sendable 优先设计”。
这一思路的关键在于对“隔离(Isolation)”的重新认识:隔离本身是一种约束。当一个类型被标记为 @MainActor,它实际上就失去了在非 UI 环境下进行同步调用的自由度。相比之下,一个非隔离、非 Sendable 的普通类型反而具有更高的通用性——它可以被任意 Actor 持有,并在其内部安全地进行同步访问,同时也更容易遵循 Equatable 等基础协议,而无需处理跨隔离域带来的复杂性。
随着 Swift 引入 NonisolatedNonsendingByDefault,这种“非 Sendable 优先”的设计路径不再像过去那样笨重或别扭,反而逐渐显现出其优势:以更少的隔离、换取更清晰的语义与更低的架构负担。这或许并非适用于所有场景,但在 Swift 6 之后,它已经成为一种值得认真考虑的、符合语言直觉的“减法”方案。
使用 Registry 加速依赖解析 (Resolving Swift Packages faster With Registry from Tuist)
传统的 SPM 依赖解析是基于 Git URL 的,Xcode 需要克隆整个 Git 仓库来获取版本信息和代码,这在依赖较多(如 Firebase)时非常耗时。而 Registry 是苹果定义的另一种规范:通过包的标识符(ID)直接下载特定版本的归档文件,跳过了繁重的 Git 操作。Tuist 最近宣布将其 Swift Package Registry 功能向所有开发者开放,最大的变化是现在无需登录或创建 Tuist 账号即可使用。
Lee Young-jun 实测发现,使用 Registry 后,依赖解析(Installation)时间缩短至原来的约 35%;但项目生成与构建阶段并未获得同等收益,甚至略有回退。在 GitHub Actions 中配合缓存使用时,二次构建的依赖安装时间则从 53s 降至 11s,优势主要体现在 CI 场景。
总体来看,Tuist Registry 并非“全流程加速器”,而是一个专注于依赖解析与缓存友好性的优化点。如果你的项目依赖数量庞大、CI 成本较高,它值得优先尝试。
iOS Timer 与 DispatchSourceTimer 选择与安全封装技巧|有限状态机防止闪退
很多开发者在处理 DispatchSourceTimer 时,最头疼的就是它那“易碎”的状态:调用顺序稍有不对便会引发闪退。ZhgChgLi 在本文中针对这种极其敏感的状态管理提出了工程化的解决方案。文章详尽列举了导致崩溃的五大常见场景(如重复 resume、suspend 状态下直接释放等),并分享了如何利用有限状态机 (FSM) 封装操作,从逻辑层屏蔽非法调用,同时配合私有串行队列确保多线程环境下的调用安全。
这是一篇引导读者从“写代码”转向“做设计”的实战案例。它不仅讲清了 GCD 定时器的正确使用方式,更展示了如何借助设计模式,将一个“危险”的底层 API,封装为语义清晰、使用安全、可长期维护的工业级组件。在 Swift Concurrency 日益成为主流的今天,理解并优雅地封装这些底层 GCD 工具,依然是高级 iOS 开发者的重要基本功。
工具
ml-sharp:照片秒变 3D 场景
苹果在上周开源了 SHARP (Sharp Monocular View Synthesis),一个能在不到 1 秒内将单张 2D 照片转换为 3D 场景的 AI 模型(模型大小 2.8 GB)。相比之前的最佳模型,视觉质量提升 25-34%,速度提升 1000 倍。
社区普遍认为 SHARP 可能用于未来版本的空间照片功能。目前 iOS 26 的 Spatial Scenes 使用 Neural Engine 进行深度重建,而 SHARP 采用更先进的 3D Gaussian Splatting 技术,质量显著提升。
模型支持 CPU/CUDA/MPS 运行,已有开发者在 M1/M2/M3 Mac 上成功运行。输出的 .ply 文件兼容各种 3DGS 查看器,Vision Pro 用户可通过 Metal Splatter 直接查看效果。
尽管苹果在通用语言大模型上不如竞争对手惊艳,但在垂直场景的 AI 模型上,凭借硬件深度整合与明确的应用导向,依然展现出强大的竞争力。
MaterialView: 突破 NSVisualEffectView 限制的毛玻璃视图
Oskar Groth (Sensei 作者)开源了 MaterialView,一个能够突破 NSVisualEffectView 限制的高度可定制毛玻璃视图库。通过逆向 Control Center 的实现,Oskar 实现了对模糊半径、饱和度、亮度和色调的完全控制,并撰写了详细的技术文章讲解实现原理。
与系统原生材质只能“选类型”不同,MaterialView 将模糊效果彻底参数化,允许开发者精确控制模糊半径、饱和度、亮度、tint 颜色与混合模式,并支持 active / inactive / emphasized / accessibility 等状态配置。这使得它非常适合用于侧边栏、浮层面板、工具窗口等对视觉一致性要求极高的场景。
该库同时支持 SwiftUI 与 AppKit,并提供了一个可实时调参的 Demo App,方便快速探索不同材质组合的效果。
需要注意的是,它依赖部分未公开的 Core Animation 能力(如
CABackdropLayer、CAFilter等)。尽管这些 API 多年来相当稳定,但仍存在未来系统版本变动的潜在风险。
![]()
往期内容
💝 支持与反馈
如果本期周报对你有帮助,请:
- 👍 点赞 - 让更多开发者看到
- 💬 评论 - 分享你的看法或问题
- 🔄 转发 - 帮助同行共同成长
🚀 拓展 Swift 视野
- 📮 邮件订阅 | weekly.fatbobman.com 获取独家技术洞察
- 👥 开发者社区 | Discord 实时交流开发经验
- 📚 原创教程 | fatbobman.com 学习 Swift/SwiftUI 最佳实践