普通视图
AT 的人生未必比 MT 更好 -- 肘子的 Swift 周报 #118
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 最佳实践