普通视图
Swift Optional几个名词备忘
Optional 的几种使用方式
1. 普通 Optional(Optional Type)
var name: String?
- 官方名称:Optional
- 推荐默认使用
2. 强制解包(Force Unwrapping)
let name: String? = "Tom"
print(name!)
- 官方名称:Force Unwrapping
- 使用
! - nil → crash
❌ 不推荐滥用
3. 可选绑定(Optional Binding)
if let name = name {
print(name)
}
或:
guard let name = name else { return }
- 官方名称:Optional Binding
- 安全解包
- ⭐ 最推荐方式
4. 可选链(Optional Chaining)
person?.address?.street
- 官方名称:Optional Chaining
- 任意一层 nil → 整体为 nil
- 安全访问链式调用
5. Nil 合并运算符(Nil-Coalescing Operator)
let name = input ?? "Default"
- 官方名称:Nil-Coalescing Operator
- 提供默认值
T! 是什么?
var name: String!
👉 官方名称:
Implicitly Unwrapped Optional
本质
Optional<String>
👉 本质仍然是 Optional,但带自动解包行为
行为
var name: String! = "Tom"
print(name) // 不需要 !
但如果name实际为 nil时,则会触发解包失败crash:
var name: String! = nil
print(name) // 💥 crash
使用场景
1. Objective-C 互操作(Objective-C Bridging)
@property NSString *name;
→ Swift:
var name: String!
2. 延迟初始化(Delayed Initialization)
典型:IBOutlet
@IBOutlet weak var label: UILabel!
风险
- 看起来像非 Optional
- 实际是 Optional
- nil 时直接 crash
👉 本质:危险语法糖
总结对照表
| 写法 | 英文名称 | 安全性 | 建议 |
|---|---|---|---|
String? |
Optional | ✅ | 默认使用 |
name! |
Force Unwrapping | ❌ | 尽量避免 |
if let / guard let
|
Optional Binding | ✅ | ⭐推荐 |
?. |
Optional Chaining | ✅ | 常用 |
?? |
Nil-Coalescing Operator | ✅ | 常用 |
String! |
Implicitly Unwrapped Optional | ⚠️ | 限定场景 |
用自定义 Layout 化解 SwiftUI List 的行高与间距跳变
动画的声明式表达是 SwiftUI 的核心优势之一。但在某些场景里,结果并不总像我们期待的那样平滑。一个典型例子是:当 `List` 行内的内容高度发生动态变化——副标题从空变为非空、文本因更新而导致行数变化——系统自带的布局引擎往往无法给出连续的过渡动画。本文从这个现象出发,逐层拆解原因,给出一种完全基于 SwiftUI 原生能力的解决方案;也借这条路径回看 SwiftUI 在布局机制层面的几个关键约束。
从社区路标到生态基石:Dave Verwer 的新篇章 - 肘子的 Swift 周报 #137
Dave Verwer 在 iOS Dev Weekly 第 751 期宣布,这份已经持续近 15 年的周报将交由新的团队继续运营,而他自己接下来会全职投入 Swift Package Index。我的博客在早期获得关注,也曾得益于 iOS Dev Weekly 的推荐;而我在周报中坚持撰写每期周评,同样在很大程度上受到 Dave Verwer 的启发。对于很多 Apple 平台开发者来说,iOS Dev Weekly 早已不只是一份链接合集。它既是社区路标,也是长期陪伴。
消失的 WWDC 愿望单 -- 肘子的 Swift 周报 #136
![]()
消失的 WWDC 愿望单
距离 WWDC 2026 只剩下 20 天了。每年到这个时候,我都会看到不少开发者分享自己的 WWDC 愿望单,写下预测与期许。但今年,至少到我汇总本期周报时,这类内容相较去年同期明显少了许多。究竟是开发者对 WWDC 的期待变淡了,还是更多人开始秉持“降低预期才能获得更多惊喜”的心理?
也许问题并不是开发者没有期待,而是旧有的愿望单形式已经不太够用了。过去,我们期待的是某个 API、某个框架、某项功能;而现在,当软件开发迅速向 AI Agent 时代靠拢时,很多期待本身也变得更难被清晰描述。
一年前,恐怕很少有开发者会预料到软件开发会如此迅速地进入 AI Agent 时代。即便我们期待 Xcode 提供更好的 AI 支持,在 Xcode 26.3 推出前,也未必会想到苹果会在 IDE 中提供与 Agent 如此紧密的集成。应用或 API 已不再只是面向消费者或开发者的接口,它们也可能成为 AI Agent 理解、调用和编排的对象。AI 不只是开发工具,也会作为新的参与者,深度进入软件服务的构建和使用过程。
我想,这也是不少开发者面对 WWDC 2026 时既期待又茫然的地方。我们希望看到更新的功能、更稳定的框架、更清晰的平台方向;同时也在思考,在这样的开发体系中,如何继续保持自己作为开发者的独特性与必要性,并与 AI 一起构建更好的服务。
WWDC 2026 究竟会带来多少变化,让我们拭目以待。
BTW:上周我非常有幸入选了苹果官方最新公布的 Apple Developer Community Spotlight。作为一名内容创作者,能够得到这样的认可,对我来说既是鼓励,也是鞭策:继续认真写下去,继续把有价值的内容带给大家。感谢每一位长期阅读、反馈和支持我的朋友。
近期推荐
从 URLSession 到电磁波:iOS 网络请求的底层原理 (URLSession to Electrons: How Networking works on iOS)
很多 iOS 开发者都知道:URLSessionDataTask 需要调用 resume() 才会真正开始请求。但在这之后究竟发生了什么?Jacob Bartlett 用一篇长文,带读者一路跟随一个普通的网络请求,从 URLSession、CFNetwork、TCP/IP、Wi-Fi,一直深入到无线电、天线与电磁波。Jacob 不仅串联起 HTTP、DNS、TCP、QUIC、IPv6 等开发者熟悉却未必真正理解的概念,也结合 iOS 内部实现介绍了 Network.framework、XNU 内核 TCP 栈、Wi-Fi 帧结构以及蜂窝网络的调度机制。
一个简单的
resume(),背后涉及的代码与协议演进跨度,可能超过十几甚至几十年。不得不感慨,现代软件世界建立在层层抽象之上,而绝大多数时候,我们其实只是幸运地生活在这些抽象足够稳定的年代。
当 AI 和 Xcode 打架时:我写了个工具来拉架
Xcode 的构建系统从未真正为“并行开发”设计过。多个任务同时构建时,DerivedData、ModuleCache、SwiftPM 缓存乃至 Simulator 都可能互相踩踏,而这一问题在 AI Agent 并行开发场景下被进一步放大。Maples7 在本文中介绍了他的解决方案:VibeChard。它基于 Git Worktree,为每个 AI Agent 创建独立的构建沙箱,并进一步隔离 DerivedData、ModuleCache、SwiftPM 缓存以及模拟器环境。最有意思的是,它并不要求开发者修改构建命令,而是通过 PATH shim 透明接管 xcodebuild,让包括 Tuist、Fastlane 在内的整条工具链都自动运行在隔离环境中。与其说这是一个单纯的辅助工具,不如说它揭示了 AI 编程时代一个更底层的问题:当代码生成速度大幅提升后,传统开发工具链的环境隔离能力也必须随之升级。
Swift 真的被搞得乱七八糟了吗?写了几年之后说点实话
Swift 变得越来越复杂,这是一个不争的事实。但这是否意味着 Swift 已经变成了一门糟糕的语言?迷途酱 从语言演进的现实约束出发,重新审视 Swift 这些年的“膨胀”。作者认为,许多被吐槽的复杂度,其实来自 Swift 同时承担应用层、系统级、DSL 宿主与服务端语言等多重目标,而并发安全与所有权模型本身,也属于计算机科学层面的“硬复杂度”。
文章既讨论了 Sendable、actor isolation、borrowing、~Copyable 等近年来快速增长的新概念,也坦率指出 Swift 在并发关键字、泛型语法以及 SwiftUI “魔法感”上的设计问题。尤其是“Swift 其实是一个语言的多个层级”这一观点,相当值得思考:绝大多数开发者日常写 App 时,其实并不需要承担全部复杂度,但 WWDC 与官方文档却经常把这些内容同时呈现在所有人面前。
Xcode Cloud 进阶:Shell 脚本自动化实战 (Writing shell scripts for Xcode Cloud)
Xcode Cloud 的上手体验已经足够简单,但在真实项目中,许多自动化需求仍然需要借助 shell scripts 完成。Amy Delves 以“在归档完成后自动创建 GitHub Release”为例,展示了如何结合 Xcode Cloud 提供的环境变量判断当前构建是否来自 main 分支的归档流程,如何读取 archive 中的版本号与构建号生成 tag,并通过 GitHub API 创建 release。虽然示例本身并不复杂,但它很好地说明了 Xcode Cloud 并不只是一个“点几下就能跑测试”的服务:借助环境变量、脚本钩子与外部 API,它同样可以承担更完整的发布自动化工作流。
我们为什么离开了很棒的 CloudKit (Why CloudKit is amazing and why we're leaving it)
这是一篇相当少见、坦诚的 CloudKit 迁移复盘。César Pinto Castillo 并没有简单批评 CloudKit,恰恰相反,他首先承认:对于小团队来说,CloudKit 几乎提供了一套“不可思议”的能力组合——免费同步、自动身份认证、端到端加密、无服务器运维,以及跨 Apple 平台的共享能力。
但随着产品逐渐发展,CloudKit 的另一面也开始显现:缺乏服务端可观测性、Schema 发布依赖手动操作、不同 Apple 平台间长期存在的同步边缘问题、对 iCloud 账户的强依赖,以及最关键的——无法真正走向 Web 与跨平台生态。最终,César 所在的团队将数据迁移到了基于 Supabase/Postgres 的同步架构。
CloudKit 是苹果生态最重要的护城河之一。但在应用越来越复杂、数据规模越来越大、用户对同步实时性的要求越来越高的今天,它的能力边界也开始显现。在追求“无感同步”的同时,我想苹果也确实需要重新认真审视这个多年未发生明显演进的基础设施了。
用 Swift 手写 LLM 训练内核 (Training an LLM in Swift, Part 1: Taking matrix multiplication from Gflop/s to Tflop/s)
在 Swift 中训练 LLM 听起来多少有些“匪夷所思”,但 Matt Gallagher 做了一次非常硬核的性能探索:不依赖现成机器学习框架,而是从手写矩阵乘法开始,一步步将基础 Swift 实现从 2.8 Gflop/s 优化到 1.1 Tflop/s。文章通过一个完整的优化过程,展示了高性能 Swift 的现实面貌:Swift 并不是不能快,但当你逐渐逼近硬件能力时,也将不可避免地进入 UnsafeBufferPointer、SIMD、并发切片、内存布局与 GPU tile 优化的世界。
这篇文章的价值并不在于鼓励开发者手写机器学习内核。恰恰相反,作者反复强调,生产环境应该优先使用 Accelerate、BNNS、Core ML、MPSGraph 等成熟框架。
当 Swift 被优化到接近 C 的程度时,它还能否保持原本的可读性与优雅?这篇文章给出了一个非常具体,也很诚实的答案。
Swift 适合写 App,但不适合训练 ML 模型 (Swift Is Great for Apps, Not for Training ML Models)
上一篇文章还在展示如何将 Swift 手写矩阵乘法一路优化到 Tflop/s,这篇文章则从另一个角度泼了盆冷水:Mohammad Azam 认为,Swift 与 Core ML 非常适合“部署”机器学习模型,但并不适合承担现代机器学习训练流程本身。Mohammad 指出,真正耗费时间的往往不是模型训练,而是数据清洗、特征工程、归一化、Pipeline 组合与实验迭代,而这些恰恰是 Python 生态最成熟、最顺手的领域。
Swift 并非不能触碰机器学习底层,但当问题从“性能”转向“数据科学工作流”后,语言与生态的重心差异便会迅速显现。这也凸显了 Swift 当前的一个困境:它具备进入多个领域的语言能力,但在应用开发之外,配套生态仍不足以支撑同等顺畅的开发体验。
工具
Swift MarkdownEngine
MarkdownEngine 是 Nodes 团队从自家 macOS Markdown 应用中抽离并开源的原生编辑器引擎。它不是 HTML 渲染器,也不是 WebView 包装,而是基于 TextKit 2 与 AppKit 构建,并桥接到 SwiftUI 的 source-style Markdown 编辑器:文本仍保持纯 Markdown,但在编辑时提供类似 Obsidian Live Preview / iA Writer 的实时样式。项目支持 wiki link、图片嵌入、代码块高亮、LaTeX、任务列表、Writing Tools,以及针对代码、公式和链接的拼写检查抑制。
TextKit 2 文档稀薄、行为细节多,而这个项目把一套已在 Nodes.app 中使用的编辑器能力开源出来,对正在开发写作、笔记或知识管理类 macOS 应用的开发者很有参考价值。
Harness:让 AI 像真实用户一样测试你的 App
Harness 是由 Alan Wizemann 开发的一款原生 macOS 开发者工具,可以驱动 iOS Simulator、macOS App 和 Web App。你用自然语言写下目标,选择一个 persona,Harness 会让 LLM agent 基于截图观察界面、执行点击和输入,并生成可回放的运行路径、成功或失败结论,以及按类型记录的 UX friction。相比单纯让 AI “操作应用”,它更像是把 AI agent、截图、事件日志、凭证脱敏、运行回放和摩擦分类整合成了一套面向开发阶段的用户测试工作台。
目前项目仍处于 alpha 阶段,Web 端依赖 WebKit,iOS/macOS 的 Set-of-Mark 定位能力还在规划中,因此更适合用于探索产品体验中的模糊点和死角,而不是替代确定性的回归测试。不过从 Swift 6、SwiftUI、SwiftData、actor 化的执行流程、JSONL run log,以及跨 Anthropic/OpenAI/Gemini 的模型抽象来看,它已经不是一个简单 demo,而是一个很值得观察的 AI-native developer tool 样本。
传统 UI 测试更擅长验证开发者预设好的路径,而 Harness 试图回答另一个问题:一个带着具体目标和身份设定的真实用户,会不会在你的界面中顺利完成任务。
往期内容
- CocoaPods 正在退场,SwiftPM 才刚到第二章 - #135
- 让 AI 从称手到称心 - #134
- Swift 并发正被更广泛地接纳 - #133
- 从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI- #132
💝 支持与反馈
如果本期周报对你有帮助,请:
- 👍 点赞 - 让更多开发者看到
- 💬 评论 - 分享你的看法或问题
- 🔄 转发 - 帮助同行共同成长
🚀 拓展 Swift 视野
- 📮 邮件订阅 | weekly.fatbobman.com 获取独家技术洞察
- 👥 开发者社区 | Discord 实时交流开发经验
- 📚 原创教程 | fatbobman.com 学习 Swift/SwiftUI 最佳实践
消失的 WWDC 愿望单 - 肘子的 Swift 周报 #136
距离 WWDC 2026 只剩下 20 天了。每年到这个时候,我都会看到不少开发者分享自己的 WWDC 愿望单,写下预测与期许。但今年,至少到我汇总本期周报时,这类内容相较去年同期明显少了许多。究竟是开发者对 WWDC 的期待变淡了,还是更多人开始秉持“降低预期才能获得更多惊喜”的心理?
CocoaPods 正在退场,SwiftPM 才刚到第二章 - 肘子的 Swift 周报 #135
谷歌近期宣布,从下一个 Flutter 稳定版 3.44 开始,Swift Package Manager 将在默认路径上取代 CocoaPods,成为 iOS 和 macOS 应用的默认依赖管理器。CocoaPods 的 Trunk 仓库计划于 2026 年 12 月 2 日正式进入只读状态——这个时间点我们在 2024 年的周报中就讨论过了,但当 Flutter 真正开始在默认路径上用 SPM 替换 CocoaPods 时,还是引发了社区的广泛热议。
第一批用 AI 写代码的 10 后,已经被苹果邀请去 WWDC 了
每年春天,苹果都会面向全球的学生出一道不太一样的命题:用 Swift 语言,做出一个有表达、有情绪、也有想法的作品。
这就是 Swift Student Challenge(SSC), 参赛者需要用 Swift 创作一个 Swift Playgrounds App,在 3 分钟的体验里,说清一个点子、完成一次交互。其中的杰出获奖者,还将在下个月的 WWDC,到 Apple Park 亲身见证属于开发者的高光时刻。
![]()
2026 年的获奖名单已经揭晓,其中不乏来自中国大陆的年轻面孔,最小的,甚至只有 15 岁。
爱范儿和其中六位获奖者聊了聊,透过他们的故事和作品,我们看到这个时代的新生代开发者,如何挥洒独一无二的灵感,用技术讲述自己的世界。
个人经历就是最好的灵感
在分享自己的作品时,几乎所有获奖者都会先分享一段个人经历,然后再介绍自己由此出发,打造了一个什么样的 App。
这些经历非常多元,有的和身心健康有关,有的只是课堂或者旅游的感想,甚至只是一次玩乐。它们的共同点是,都存在着一个还没被解决的「问题」。
也因为这些作品都和个人感受高度相关,这些作品给人的印象,已经远远不止于「作品展示」,更让我看到它们具有真正上架 App 的潜力,并不禁去想,为何在这之前,没有人做这些应用?
![]()
具体困境,写进产品
这一组作品,都源于参赛者对日常的敏锐洞察。他们捕捉到了特定群体那些隐而未现的困境,在现有工具匮乏的现状下,他们选择拒绝等待,亲手为这些被忽视的需求定制了数字化的解决方式。
赵经纬 PMS.aid
赵经纬是一名典型的「行动者」。
她接触 Swift 的原因很简单:当她想把自己拍的猫猫视频剪辑、整理成实况照片分享,却发现市面上缺乏好用的 App,于是决定自己开发一个。
她的参赛作品 PMS.aid,也有着类似的创作背景。
![]()
赵经纬的朋友患有经前焦虑症(PMDD),表现为月经来潮时反复出现的情绪波动、躯体不适和行为障碍。
当她就诊时,想向医生展示自己的症状和心情变化,却发现目前缺乏了一个应用,能将和 PMDD 有关的数据集中展示。
于是,赵经纬就开发了 PMS.aid,专门面向 PMDD 和 PMS(经前综合征)患者,能够将月经、心情、日记等数据全部放在同一条周期时间线上,零散的数据成为完整的治疗方案。
![]()
赵经纬原本学习公共管理专业,后面发现自己更想要去尝试将人文社科和技术相结合,于是转至计算机专业,未来也将继续攻读人机交互的硕士学位。
她告诉爱范儿,自己已经定下了一个「开发者目标」——为每个人设计(Made for everyone)。
沈宸颉 Help the Bajau
沈宸颉的参赛作品 Help the Bajau,萌芽于一次志愿旅行。
今年 2 月,他在马来西亚仙本那进行志愿活动,接触到了被称为「海上吉普赛人」的巴瑶族社区,这群以海为家的原住民,正在面临严重的海洋环境污染问题。
于是他开发了沉浸式交互游戏 Help the Bajau,以巴瑶族的真实故事为背景,并参考了学术论文和巴瑶族社区个案,还使用了自己拍摄的真实素材。
![]()
AI 在他的创作过程中扮演了很重要的角色:刚刚接触 Swift 时,AI 回答了很多知识点的问题;Help the Bajau 的一些视觉元素,也是利用 AI 创造的。
但沈宸颉认为,工具只是一种辅助, 用心打磨每一个细节才能成就有温度的作品;技术也不只是冰冷的代码,它们在切实地改变世界。
赵芯澄 Orat
赵芯澄的开发者之路,源自于他的父亲的行动——没有任何开发经验,却利用 Swift UI 开发了一个 App 并上架 App Store,这给他带来了很大的鼓舞。
他的参赛作品 Orat,则植根于同学的真实情景:一次课堂展示,同学因为焦虑完全忘词,他想帮同学改变这种窘况,却没能在 App Store 找到简单好用的应用,于是决定自行开发。
Orat 是一款帮助用户智能训练演讲能力的 App,利用手势、姿态和语音识别,不断引导用户练习,并会生成相应的报告。
![]()
交互创新,前沿探索
这三位获奖者,更聚焦在「交互」的创新上,或许是一种对人机操作可能性的全新探索,又或许是数字化的方式,重写现实生活遇到的不快。
吴天禹 MagiBotics
「具身智能」火了有一段时间,机器人都连着上了两年春晚,但似乎我们和机器人之间,还隔着实验室的玻璃墙。
作为一名机器人专业的博士生,吴天禹的参赛作品「MagiBotics」,就是为了打破这种隔阂。
MagiBotics 通过简明易懂的交互方式,设置了三道颇具游戏感的关卡,用户在使用 App 的过程中,学习到机械臂的三种运动方式,并且最后还能利用 AR 技术,将用户设定好动作的「机械臂」,投射到自己面前。
![]()
吴天禹也告诉爱范儿,未来他将继续专注人机交互的方向,尤其希望能利用我们每个人都有的手机,搭建一条通往具身智能的桥梁,将这项前沿技术带给更多普通用户。
付佳鹭 Maestro
用「斜杠青年」来形容付佳鹭,最恰当不过:主修物理专业,出于兴趣辅修了计算机,制作过几台能避障、能语音控制的 AI 智能小车,也开发过记账和日记应用。
她的参赛作品 Maestro,却是一个「音游」。
在这个画风可爱的小游戏中,玩家将扮演「小熊指挥官」, 管理一支由4名小动物乐手组成的乐队,通过手部动作和面部表情,指挥乐队完成一次「即兴演奏」。
![]()
这种非常富有创意和趣味的交互方式,或许与付佳鹭自己和苹果产品的体验息息相关——她被 Swift Playground 编程平台的交互式教学吸引「入坑」,又被 Apple Vision Pro 的体感和沉浸式交互体验所震撼。
严禹 Pixel Beader
严禹接触 Swift Playground 的契机,源于短视频,他发现 Apple 开发者所使用的 Xcode,左边窗口刚刚键入代码,右边的画布就能实时显示变化,这种开发者交互模式深深吸引了他。
他开发参赛作品 Pixel Beader 的动机,则同样来自他的日常生活。
严禹尝试了最近很火的「拼豆」,却发现并没有想象中那么好玩——碰翻豆板就要全部重做,熨烫塑料会散发刺鼻气味,久坐还会腰痛。
![]()
于是他的参赛作品,就是一个「赛博拼豆」项目 Pixel Beader,用 iPad 和 Apple Pencil,创造了一个无害零损耗的虚拟创作空间,用户可以将任意图片转换成拼豆图纸,打造和收藏数字的 3D 拼豆作品。
AI 时代,编程能力更珍贵了?
过去两年,「编程」这个原本的手工艺活,正在被 AI 深刻改写。
去年推出的 Claude Sonnet 4 和 GPT-5-Codex,AI 可以像真正的工程师一样理解整个项目、自主完成测试、调试迭代,开发者只需要下指令。
作为参赛者中开发经验比较丰富的吴天禹,他深度经历了这个技术变迁。以前学编程,他只能依靠网上搜索、论坛查代码、看教学视频的「原始」方式学习编程和改进作品;而现在,他觉得自己更像一个「产品经理」,大部分代码可以交给 AI 来编写,效率提升数倍。
![]()
苹果对于「AI 编程」的态度,相当开明。今年 2 月推出的 Xcode 26.3 版本中,已经直接集成了 Claude Agent 和 OpenAI Codex。
即使作为一场竞赛,Swift Student Challenge 也并不将 AI 定性为作弊的「外挂」,反而拥抱 AI 在比赛中的运用。
像是两位中学生获奖者沈宸颉和赵芯澄,他们本身就有 Python、C++ 等编程语言的学习经历,AI 帮助他们大大降低了 Swift 的门槛,帮助他们实现技能的快速迁移,他们认为这很好补足了他们作为非专业开发者所欠缺的经验,并且由于 AI 带来的效率提升,可以花更多时间和精力,打磨应用要传达的理念。
Apple 开发者关系国际市场负责人谢恩伟认为,对于学生将想法付诸实践的能力,AI 工具是一次赋能,非常欢迎学生使用 AI 工具来调试代码。
![]()
甚至这也成为了竞赛的一种考察:今年开始,参赛者需要在参赛文档中分享使用工具的经验,确保最终作品体现出来的是自身的替代性思维和创造力,而非简单依赖 AI。
不管是谢恩伟,还是学生参赛者们,都一致认为,即使 AI 让「vibe coding」这种编程方式成为可能,学习编程依旧很有必要,甚至更有必要。
谢恩伟告诉爱范儿,比起以往任何时候,现在学习编程更有意义:
有一些非常核心的东西,比如对编程架构的理解、创意的火花,以及那些从未被验证过的全新思路,都需要发挥人类的聪明才智。
学习编程,实际上就是学习解决问题的语言,这种技能的价值,在于让你的创意变成实践。
这六名获奖者,或多或少都具有计算机的知识背景,即使是两名中学生,都学习过 C 语言,参加过不少相关的课后活动。
已经借助 AI 编程,让自己的作品上架 App Store 的吴天宇认为,想要把 AI 用好,本身还是需要学习相关的知识,理清做 App 的框架和逻辑,才能把提示词给写好。
说到底,AI 能力的上限,取决于用户给他投喂的提示词质量。
特别是在未来,当 vibe coding 更加普及,这不意味着所有人都能用这种「小白」的方式,打造一个优秀的产品,想要在同质化的 App 中脱颖而出,更需要懂编程,有表达。
![]()
付佳鹭则从另一个角度看待「学编程」的意义:本质上也是一种「有趣的思维训练」,学习一种解决问题的解决方式。
她尝试过 vibe coding,惊叹于它的低门槛,惊喜于自己能实现更多的想法,却也担心人们以后都会「少想一步」:
这或许是科技进步时,我们必须要面对的一个权衡问题。或者说,我们更需要解决的是,如何正确地利用 AI,来加速自己的「主动」思考,用一种巧妙的方式,将自己的硬性能力、创造力、生产力都提升,这或许是一个更好的愿景。
AI 可以为你编写代码,但它无法理解你应用程序的核心逻辑,也无法理解某些设计决策的重要性。
严禹则认为,既然 AI 将门槛降低,决定为什么做,比怎么做更重要:
具体内容的实施可以由 AI 接管,但创作者的竞争力将愈发取决于对底层原理的理解、对系统架构的判别,以及对审美的认知。AI是没有办法替代开发者的个人创作的,就好比训练数据,人工创作的数据质量往往比AI生成的要高得多,所以我相信在未来原创性的思考与产品会更加珍惜与可贵。
刚好,这就是 Swift Student Challenge 举办的宗旨。
这本来就不是一场常规的「竞赛」,它没有一道道需要解决的编程难题,也不看重作品背后代码的复杂程度, 苹果甚至不需要你开发一个完整的应用,只是想看看你能用 Swift 代码,做出怎样的自我表达。
![]()
对于这一批未来开发者的新生血液,在未来开发之旅中, AI 的角色注定会越来越重要,甚至能接管整个开发过程的编写工作。
凭借在开发者和业界影响力,苹果能够以 Swift Student Challenge 的全球竞赛,为摩拳擦掌的准开发者们,上好第一课:
「技术」只是应用诞生的前提,「表达」才是应用脱颖而出的根本。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
让 AI 从称手到称心 - 肘子的 Swift 周报 #134
从开始深度使用 AI 工具至今已有三年。三年间,我亲历了 AI 能力的飞跃,也越来越清晰地触摸到它的边界。截至目前,AI 早已是非常出色的效率工具,但如何让它写出真正“称心”——符合我个人风格、想法与设计哲学——的代码,仍是一个不小的挑战。
HelloGitHub 第 121 期
Swift 并发正被更广泛地接纳 - 肘子的 Swift 周报 #133
从 Swift 5.5 引入符合现代编程思想的新并发模型算起,一转眼快 5 年了。从 5.5 到目前的 6.3,Swift 社区一直在采用小步迭代的方式,积极推进并发 API 的演进。但在应对过多的新关键字、复杂的隔离概念以及一些容易引发困扰的“反模式”时,这个过程对开发者来说并不算顺利。
数据持久化与缓存策略:在离线与在线间架起桥梁
引言:数据无处不在,存储何去何从?
在现代移动应用中,数据如同血液般流淌于每个功能模块之间。然而,网络并非永远可靠,用户期待的是无缝的体验——无论在地铁隧道中、飞行模式下,还是在信号微弱的乡村。这种期待催生了对数据持久化与缓存策略的深度思考。一次关于本地数据丢失的故障排查,让我们意识到:数据的生命周期管理远比简单的"保存与读取"复杂得多。本文将从实际案例出发,探讨如何构建一个既能保证数据一致性,又能提供流畅离线体验的存储架构。
一、存储方案的选择:从UserDefaults到数据库的演进之路
// 初级做法:滥用UserDefaults
UserDefaults.standard.set(userProfile, forKey: "currentUser")
UserDefaults.standard.set(accessToken, forKey: "authToken")
UserDefaults.standard.set(products, forKey: "cachedProducts")
然而,UserDefaults本质上是一个plist文件,适合存储配置信息和小量数据,但不适合存储复杂对象或大量数据。当应用需要存储用户聊天记录、商品目录或离线文章时,我们需要更专业的解决方案。
下图展示了不同存储方案的选择路径,帮助开发者根据数据特性做出合理决策:
![]()
二、架构核心:构建统一的数据访问层
随着应用复杂度增加,直接在各种业务模块中操作不同存储方案会导致代码高度耦合。更好的做法是构建一个统一的数据访问层(Data Access Layer),为上层业务提供一致的接口。
// 统一存储协议
protocol DataStorageProtocol {
associatedtype T
func save(_ item: T, forKey key: String) -> AnyPublisher<Void, StorageError>
func load(forKey key: String) -> AnyPublisher<T, StorageError>
func delete(forKey key: String) -> AnyPublisher<Void, StorageError>
func clear() -> AnyPublisher<Void, StorageError>
}
// 具体实现:UserDefaults存储
class UserDefaultsStorage<T: Codable>: DataStorageProtocol {
private let userDefaults: UserDefaults
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
func save(_ item: T, forKey key: String) -> AnyPublisher<Void, StorageError> {
return Future<Void, StorageError> { promise in
do {
let data = try self.encoder.encode(item)
self.userDefaults.set(data, forKey: key)
promise(.success(()))
} catch {
promise(.failure(.encodingFailed))
}
}.eraseToAnyPublisher()
}
}
这种抽象带来了多重好处:业务代码无需关心底层是使用UserDefaults、Core Data还是文件系统;存储实现可以独立替换;统一的错误处理;以及易于测试的接口。
三、缓存策略:智能数据的生命周期管理
缓存不仅仅是"保存一份数据副本",而是需要精心设计的策略。一个完整的缓存系统需要考虑以下维度:
- 缓存粒度:是按页面缓存、按接口缓存,还是按数据实体缓存?
- 失效策略:基于时间(TTL)、基于事件(数据更新),还是混合策略?
- 存储位置:内存缓存、磁盘缓存,还是多级缓存?
- 同步机制:如何保证缓存与服务器数据的一致性?
让我们设计一个支持多级缓存的智能系统:
class SmartCacheManager {
// 内存缓存(快速但易失)
private let memoryCache = NSCache<NSString, NSData>()
// 磁盘缓存(持久但较慢)
private let diskStorage: DataStorageProtocol<Data>
// 网络层用于刷新数据
private let networkService: NetworkServiceProtocol
func fetchData<T: Codable>(for key: String,
maxAge: TimeInterval = 300, // 默认5分钟
forceRefresh: Bool = false) -> AnyPublisher<T, Error> {
// 1. 检查是否需要强制刷新
guard !forceRefresh else {
return fetchFromNetwork(key: key)
}
// 2. 检查内存缓存
if let cachedData = memoryCache.object(forKey: key as NSString) as Data?,
let cachedItem = decodeData(cachedData) as T? {
return Just(cachedItem)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
// 3. 检查磁盘缓存
return diskStorage.load(forKey: key)
.tryMap { data in
// 检查缓存是否过期
if self.isCacheValid(for: key, maxAge: maxAge) {
return try JSONDecoder().decode(T.self, from: data)
} else {
throw CacheError.expired
}
}
.catch { _ in
// 4. 缓存无效或不存在,从网络获取
return self.fetchFromNetwork(key: key)
}
.eraseToAnyPublisher()
}
}
下图展示了智能缓存系统的工作流程,从数据请求到返回的完整决策链:
![]()
- 冲突解决:当本地修改与服务器数据冲突时如何处理?
- 增量同步:如何高效地只同步变化的数据?
- 同步状态管理:如何向用户展示同步进度和状态?
我们可以设计一个基于操作队列的同步管理器:
class SyncManager {
private let operationQueue = OperationQueue()
private let pendingOperationsStorage: DataStorageProtocol<[SyncOperation]>
// 记录待同步的操作
func enqueueOperation(_ operation: SyncOperation) {
// 保存到本地,确保即使应用崩溃也不会丢失
var pendingOps = (try? pendingOperationsStorage.load(forKey: "pending")) ?? []
pendingOps.append(operation)
pendingOperationsStorage.save(pendingOps, forKey: "pending")
// 添加到操作队列
operationQueue.addOperation(operation)
}
// 监听网络状态变化
func setupNetworkObserver() {
NotificationCenter.default.publisher(for: .networkReachable)
.sink { [weak self] _ in
self?.retryPendingOperations()
}
.store(in: &cancellables)
}
}
这种设计确保了即使用户在离线状态下进行操作,这些操作也会被安全地保存,并在网络恢复时自动同步。
五、性能优化:存储的效率与安全平衡
数据持久化不仅关乎功能,更直接影响应用性能。我们需要在多个维度上寻找平衡点:
- 读写性能:大量小文件 vs 少数大文件
- 内存占用:缓存大小限制与淘汰策略
- 电池消耗:磁盘IO对电池寿命的影响
- 数据安全:敏感信息的加密存储
对于敏感数据如用户凭证,我们应使用iOS的Keychain服务:
class SecureStorage {
func saveSecureItem(_ item: String, forKey key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: item.data(using: .utf8)!
]
SecItemDelete(query as CFDictionary) // 先删除旧项
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
}
对于大量数据的存储,我们需要考虑分页加载和懒加载策略,避免一次性加载过多数据导致内存压力。
六、总结:构建可靠的数据基石
数据持久化与缓存策略是移动应用架构中最为基础也最为复杂的一环。它不仅仅是技术选择的问题,更是对用户体验、性能表现和安全保障的综合考量。
通过构建统一的数据访问层,我们实现了存储实现的解耦;通过智能缓存策略,我们平衡了性能与数据新鲜度;通过离线优先的同步机制,我们确保了应用的可用性;通过性能优化措施,我们保障了应用的流畅运行。
这再次印证了本系列文章的核心思想:优秀的架构设计在于预见复杂性并提前规划应对策略。当数据层稳固可靠时,上层业务开发便能够专注于创造价值,而不必担心数据丢失、同步冲突或性能瓶颈。在数据驱动的时代,一个精心设计的数据持久化架构,是应用成功的基石,也是技术卓越的体现。
别被系统绑架:SwiftUI List 替换背后的底层逻辑
![]()
凌晨三点,楼里只剩空调低鸣。林屿坐在工位前,盯着
SwiftUI里的List,像盯着一个多年的老朋友。这个老朋友不坏,甚至称得上可靠。可今天,他忽然觉得不对劲了。页面能跑,交互也顺,但那层说不清的“高级感”,总像隔着一层雾,伸手能碰到,握住却没有。问题出在哪?他顺着代码往下摸,摸到最后,才发现真正的悬念从来不在样式,而在工具选错了场子。
🧭 在 SwiftUI 中构建 List 的替代方案
每当你打算在 SwiftUI 里做一个可滚动页面时,第一反应往往是用 List。这很正常。List 名声在外,又是系统组件,出手就带着几分正统气息。
![]()
但话说回来,它并不总是最合适的选择。
List 最擅长的,是展示那种整齐划一的统一数据。比如邮箱列表、待办事项、联系人,这类内容结构规整,节奏统一,List 处理起来得心应手。
可如果不是这种场景,画风就变了。
对于其他更灵活、更讲究布局和视觉层次的页面,ScrollView 搭配 lazy stack,几乎总是更好的方案。
![]()
这篇文章要讲的,就是如何在 SwiftUI 中构建一个自定义的可滚动容器,让我们对 look and feel 拥有真正精细、可控的掌握力。
⚙️ 先把底牌摊开:ScrollView 这几年,已经不是昨日黄花
先说一句实在话。
过去几年里,SwiftUI 对 ScrollView + lazy stacks 的性能做了相当大的改进。它早已不是那个“能用,但心里发毛”的角色。今天的它,已经足够稳,足够快,也足够灵活。
![]()
所以,如果你展示的不是那种几十万条统一数据,比如邮箱、待办清单这种超大规模列表,那么:
ScrollViewis a way to go.
这句话轻描淡写,实际上意味深长。
它的意思不是 “List 不行”,而是:
如果你的页面不依赖大规模统一数据的复用机制,那你就没必要把自己绑死在 List 上。
工具有长处,也有边界。看不见边界,迟早吃亏。
🫀 CardioBot 的现状:已经不错,但还不够狠
这是林屿自己独立开发的 CardioBot app。
![]()
上面有 4 张截图:前两张是当前版本,后两张是林屿想达到的效果。
现在这款 app 使用的是标准 List。而且说句公道话,它现在的界面观感并不差,作者自己也很喜欢它目前的 look and feel。
但人一旦开始较真,就回不了头。
![]()
林屿决定重新审视自己的 UI。目标并不激进,不是要把界面改得面目全非,而是要做到两件事:
- 保留 iPhone 用户熟悉、直观、可识别的感觉
- 让 UI 再精致一些,再讲究一些,再“骚”一点,但绝不轻浮
这类优化最难。它不是“重做”,而是“进一寸”。可往往,真正拉开差距的,就是这一寸。
🧱 为什么这里的 List 已经不再对味了
CardioBot 展示的是不同类型的健康指标。问题在于,这些内容不是统一数据集,而是一组风格不同、职责不同的内容块。
林屿用了多种 card 类型,比如:
HeroCardTintedCardRegularCard
看到这里,症结就露出来了。
如果数据并不统一,那么使用 List 去做 cell recycling,其实就没多大意义。List 的一身本事,主要是为海量、统一、标准化的数据而生。可这里是一桌散席,不是整齐列队。
![]()
林屿当然也试过继续依赖 List。
他可以通过一些 list-specific view modifiers 做出接近目标的样子,比如:
listRowBackgroundlistItemTintlistRowInsets
它们在 List 内部确实很好使,像一把趁手的短刀。
可惜,刀再锋利,也有出鞘范围。
这些 list-specific view modifiers 一旦离开 List view,立刻失灵。也就是说,这些能力是 List 私有的,不可外借。
![]()
结果就是:
你想在 List 之外维持相同风格,就必须额外补样式。补来补去,补成了拆东墙补西墙,最后不是代码发虚,就是视觉跑偏。
这就不是“能不能做”的问题了,而是“做得值不值”。
🪄 真正的转机:Container View APIs
幸运的是,SwiftUI 后来引入了 Container View APIs。
这套 API 看起来安安静静,实际上杀伤力很大。它允许我们把 SwiftUI 视图先拆解,改点东西,再重新组合回来。
这意味着什么?
![]()
意味着你不再只是“使用容器”,而是可以“制造容器”。
你可以借助 Container View APIs 构建可复用的容器视图,像 List、Form,甚至任何高度自定义的东西。
说穿了,这是一种权限的变化。
以前你在用系统给的积木。
现在你很Happy的开始自己烧砖。
📦 第一块积木:ScrollingSurface
由于林屿的 app 中每个页面都采用 ScrollView 加 lazy stack,所以他提炼出了一个统一类型:ScrollingSurface。
public struct ScrollingSurface<Content: View>: View {
public enum Direction {
case vertical(HorizontalAlignment)
case horizontal(VerticalAlignment)
}
let direction: Direction
let spacing: CGFloat?
let content: Content
public init(
_ direction: Direction = .vertical(.leading),
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.direction = direction
self.content = content()
}
public var body: some View {
switch direction {
case .horizontal(let alignment):
ScrollView(.horizontal) {
LazyHStack(alignment: alignment, spacing: spacing) {
content
}
.scrollTargetLayout() // 告诉滚动系统:这里是目标布局区域
.padding()
}
case .vertical(let alignment):
ScrollView(.vertical) {
LazyVStack(alignment: alignment, spacing: spacing) {
content
}
.scrollTargetLayout() // 垂直方向同理
.padding()
}
}
}
}
他的意思很直接:ScrollingSurface 本质上就是对 ScrollView 和 LazyVStack / LazyHStack 的一个简单包装。根据方向不同,切换成垂直或水平滚动容器。
![]()
但别小看这个“简单”。
为什么它值得单独抽出来?
因为它做了三件很重要的事:
- 统一了页面根结构
- 统一了滚动方向的表达方式
- 统一了 spacing 和 padding 的布局语义
林屿会把 ScrollingSurface 作为 app 每个页面的 root view。
这不是偷懒,这是定规矩。
![]()
规矩一旦立住,后面的样式和结构才能不乱套。
🃏 第二块核心积木:DividedCard
接下来,UI 里的关键原语出现了:DividedCard。
它最重要的地方,在于使用了 Group(subviews:),这是 SwiftUI Container View API 的一部分。
public struct DividedCard<Content: View>: View {
let content: Content
public init(@ViewBuilder content: () -> Content) {
self.content = content()
}
public var body: some View {
Group(subviews: content) { subviews in
if !subviews.isEmpty {
VStack(alignment: .leading) {
ForEach(subviews) { subview in
subview
if subviews.last?.id != subview.id {
Divider()
.padding(.vertical, 8) // 在每个子视图之间插入分隔线
}
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(
.regularMaterial,
in: RoundedRectangle(cornerRadius: 32) // 给整体包上圆角卡片背景
)
}
}
}
}
Group(subviews:) 到底妙在哪?
这招很关键。
它允许我们把通过 @ViewBuilder 传进来的视图拆成一个个子视图。
![]()
换句话说,你不再只能把一整坨内容当黑盒来用,而是能看到里面每个子项,并逐个处理它们。
林屿在 DividedCard 里干的事情很漂亮:
- 先把内容拆开
- 遍历所有
subviews - 在每个子视图后面加上
Divider,但最后一个不加 - 最后把整个结构包进一个带圆角的材质背景里
结果就是:
一组原本只是“连续排列的内容”,立刻拥有了卡片感、分组感和边界感。
这一手为什么重要?
因为很多产品界面都存在这样的结构:
- 一张卡片里放多个入口
- 每个入口既独立,又需要视觉连续
- 中间要有分隔,但不能显得生硬
以前你可能要在每个地方重复写 Divider、padding、background、cornerRadius`,写多了就腻,改起来更烦。
![]()
现在不同了。DividedCard 把这套规则提炼成了一个可复用 primitive。
这就是架构的味道:
不是“这页看着对”,而是“以后都能对”。
🧩 第三块积木:SectionedSurface
另一个很有意思的 UI primitive,是 SectionedSurface。
public struct SectionedSurface<Content: View>: View {
let content: Content
public init(@ViewBuilder content: () -> Content) {
self.content = content()
}
public var body: some View {
ForEach(sections: content) { section in
if !section.content.isEmpty {
section.header.padding(.top) // 给 section 的 header 增加顶部间距
section.content
section.footer
}
}
}
}
它使用了 ForEach(sections:),这个能力可以从传入的视图中提取所有的 Section,然后做统一处理。
林屿这里做了两件事:
- 过滤掉没有内容的 section
- 给 section header 增加一些顶部间距
这看着朴素,实际上很实用。
![]()
因为在真实业务里,section 常常是动态的。
某块有数据,就该显示;没数据,就该消失。
如果每个页面都自己处理一遍这些逻辑,迟早会写成一锅粥。
而 SectionedSurface 把这类规则直接吸收到了容器层。
页面只负责描述内容,容器负责决定组织方式。
这就叫分寸。
代码里有分寸,界面就不会失态。
➡️ 离开 List 后,NavigationLink 的箭头去哪了?
很多人一旦不用 List,很快就会发现少了点什么。
没错,就是 NavigationLink 右侧那个熟悉的小箭头,也就是 chevron。
在 List 中,它会自动出现在 trailing edge,系统帮你安排得明明白白。可一旦离开 List,这个默认样式就没了。
![]()
林屿的办法很干脆:写一个自定义 ButtonStyle。
public struct NavigationButtonStyle: ButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.label
.opacity(configuration.isPressed ? 0.7 : 1) // 按下时微微变淡,增加反馈感
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.tertiary) // 补回 List 风格的右侧箭头
}
.contentShape(.rect) // 扩大点击区域,让整行都可点
}
}
extension ButtonStyle where Self == NavigationButtonStyle {
public static var navigation: Self { .init() }
}
这一招的好处在于,它不是临时补救,而是顺势把“导航型按钮”的风格单独抽了出来。
![]()
以后只要写:
.buttonStyle(.navigation)
整页涉及导航的按钮,就能统一表现。
这才像回事。
高手不是把洞补上,高手是顺手把墙也砌直。
🏗️ 实战拼装:SummaryView
下面这段代码,展示了前面这些新原语在 app 中的实际用法。
public struct SummaryView: View {
let summary: SummaryStore
public var body: some View {
ScrollingSurface {
SectionedSurface {
coachSection
activitySection
recoverySection
vitalsSection
heartRateSection
alcoholicBeveragesSection
}
}
.buttonStyle(.navigation) // 统一套用导航按钮样式
}
@ViewBuilder private var activitySection: some View {
Section {
if !summary.metrics.workouts.isEmpty {
DividedCard {
ForEach(summary.metrics.workouts, id: \.workout.uuid) { snapshot in
NavigationLink {
WorkoutDetailsView(snapshot: snapshot)
} label: {
WorkoutView(snapshot: snapshot)
}
}
}
}
} header: {
SectionHeader(
.horizontal,
title: Text("activitySection"),
systemImage: "figure.run"
)
.tint(.orange)
}
}
}
这一段真正漂亮的地方在哪?
表面上看,它的使用方式和 List API 非常像:
- 有
Section - 有
NavigationLink - 有 header
- 有内容分组
但底层已经换了天地。
![]()
林屿通过:
ScrollingSurfaceDividedCardSectionedSurfaceNavigationButtonStyle
重新拼出了类似 List 的使用体验,同时拿回了对 look and feel 的精准控制。
更妙的是,如果某个页面压根不需要 sections,只要把 SectionedSurface 去掉即可,其余 primitive 仍然能继续复用。
这就说明它们不是页面特供,而是真正的可复用 building blocks。
![]()
到了这一步,已经不是“替代 List”那么简单了。
这是在搭自己的界面语言。
真相大白:弃用 List 非叛逆,懂了取舍是清醒
最后,林屿把话说得很准。
在 SwiftUI 里替换 List,并不是要放弃一个强大的组件。它真正要表达的是:
不是背叛
List,而是为场景选择正确的工具。
如果你面对的是大型、统一的数据集,List 依旧是极好的选择,毫无问题。
![]()
但当你的 UI 需要更细致的结构、更独特的样式、更符合产品自身 design language 的表达时,现代 SwiftUI 已经给了我们足够的自由。
借助 ScrollView、lazy stacks 和 Container View APIs,我们不只是可以重建 List 的能力,某些时候甚至能够超越它。
像 ScrollingSurface、DividedCard、SectionedSurface 这样的自定义 primitive,证明了一件事:
真正成熟的 SwiftUI 代码,不只是把视图摆出来,而是把可复用的规则提炼出来。
性能、清晰度、设计语言,三者并行不悖。
这才是正路。
![]()
🌒 尾声:他最终没有推翻 List,只是看透了它
天快亮的时候,林屿合上电脑,办公室的灯光仍旧冷,心里却亮了。
他没有把 List 当成敌人。
也没有为了“自定义”而自定义。
![]()
他只是终于明白:
组件从来不是信仰,它只是工具。
该用 List 的时候,别拧巴;
该用 ScrollView 和自定义容器的时候,也别手软。
很多人写 UI,写到最后,写成了对系统组件的依赖。
可真正厉害的人,写到最后,会慢慢长出自己的容器、自己的规则、自己的语言。
![]()
那一刻,所谓 List replacement,其实已经不重要了。
重要的是,他终于从“会用组件”,走到了“会造秩序”。
而这,才是这篇文章最狠的一刀。
Swift 核心协议揭秘:从 Sequence 到 Collection,你离标准库设计者只差这一步
swift是面向协议编程,果然名不虚传
IteratorProtocol 协议
public protocol IteratorProtocol<Element> {
associatedtype Element
mutating func next() -> Element?
}
这样所有遵守了IteratorProtocol协议的类型,都是可以使用next方法的,这已经很完美了。但是!迭代器只能消费一次, 这里举一个不恰当的例子:
let numbers = [10, 20, 30]
// 从序列要一个迭代器(IteratorProtocol)
var it = numbers.makeIterator()
// 一步一步消费
print(it.next() as Any) // Optional(10)
print(it.next() as Any) // Optional(20)
print(it.next() as Any) // Optional(30)
print(it.next() as Any) // nil —— 已经到头了
// 同一个 it 再 next,永远是 nil(状态已经走到结束)
print(it.next() as Any) // nil
print(it.next() as Any) // nil
想再从头遍历一遍,不能指望复活这个it,只能再向序列要一个新的迭代器:
var it2 = numbers.makeIterator()
print(it2.next() as Any) // Optional(10) —— 又从第一个开始
但是这里的makeIterator是sequence协议要求提供的东西,之所以说这个例子不恰当,是因为我似乎在用已经解决的问题去回答问题,这里不应该把sequence牵涉进来。
那么,接下来的例子将非常合适。
struct CountFromTo: IteratorProtocol {
var current: Int
let end: Int
init(from: Int, through: Int) {
current = from; end = through
}
mutating func next() -> Int? {
guard current <= end else { return nil }
defer { current += 1 }
return current
}
}
var it = CountFromTo(from: 3, through: 5)
while let x = it.next() { print(x) } // 耗尽
print(it.next()) //nil,因为之前已经耗尽了
// 不能复活it,只能再来一个新的迭代器实例
var it2 = CountFromTo(from: 3, through: 5)
print(it2.next() as Any) // 又从 3 开始
var it = CountFromTo(from: 3, through: 5)
现在假设我们是swift标准库团队开发人员,要实现Array,我们需要提供给开发者类似以下这些功能
for x in arrarr.map { }、arr.filter { }的功能和别的“能挨个读一遍某个东西”的方法用同一套API
下标 arr[i]可以实现“挨个读一遍”的功能,但是正如我们提到的for x in arr / arr.map这种功能,它们只想对每个元素做某事,不需要关心下标。
for x in arr {
print(x)
}
//可以通过这种方式实现
var __iterator = arr.获取iterator()
while let x = __iterator.next() {
print(x)
}
map大致如下
func mapSimple<T>(_ transform: (Element) -> T) -> [T] {
var result: [T] = []
var it = 获取iterator()
while let x = it.next() {
result.append(transform(x))
}
return result
}
可以看出不论实现哪个功能都需要array有一个获取iterator的方法,给这个方法起名叫做makeIterator,也就是说array既要有next方法,又要有makeiterator的方法,我们把这两个方法都放入一个起名为sequence的protocol中,这就是sequence的由来了。Sequence 是 Swift 中最轻量的遍历协议。一个类型只要遵守 Sequence,就能用 for-in 遍历。实现了 Sequence的结构体或类 必须关联一个遵守 IteratorProtocol 的类型,
-
Sequence是工厂:生产迭代器 -
IteratorProtocol是产品:实际遍历逻辑
所以不能说实现了Sequence就是是实现了IteratorProtocol.
仅仅实现Sequence协议,你的类型就能享受所有Sequence的默认extension方法:map、filter、reduce、contains(Element: Equatable)、reversed。
//Sequence 协议:
protocol Sequence<Element> {
associatedtype Element where Self.Element == Self.Iterator.Element
associatedtype Iterator: IteratorProtocol
func makeIterator() -> Iterator
}
Sequence 够用了吗?
Sequence 只保证:能 makeIterator(),按顺序 next() 一个个拿。
适合:for-in、map、filter 等扫一遍的事。
但日常还会遇到:
-
第 3 个元素是谁?(随机访问某一位)
-
有多少个?(count)
-
第一个、最后一个下标怎么表示?
只靠 Iterator:只能往后走,不能跳到中间,也不一定有常数时间的长度概念(有些序列是无限的、或算长度很贵)。
所以要在 Sequence 上再叠一层:能按下标(或索引)访问、有明确首尾——这就是 Collection 的由来。
Collection 在解决什么?
在能遍历之上,再约定像容器一样用下标访问的能力。典型能力包括(概念上):
-
有
startIndex/endIndex -
能用
collection[index]读元素(subscript) -
索引可以
index(after:)往后走(不一定只是Int + 1,字符串的Index就复杂) -
往往还能提供
count(有的集合是 O(n) 算出来)
public protocol Collection: Sequence {
associatedtype Index: Comparable
var startIndex: Index { get }
var endIndex: Index { get }
subscript(position: Index) -> Element { get }
func index(after i: Index) -> Index
}
`Collection` 协议**继承自** `Sequence` 协议,因此任何遵守 `Collection` 的类型**自动满足** `Sequence` 的所有要求。
Array 是最典型的 Collection:下标是 Int,从 0 到 count-1。
Sequence ←── 更基础:只保证能遍历
↑
Collection ←── 继承 Sequence,并加:索引 + 下标访问 + …
![]()
在 Swift 里遵守 Collection 只能说明它是可按索引访问的一段序列,不一定是自己拥有一块独立存储的容器。例如Range,遵守RandomAccessCollection属于 Collection 一族
let r = 0..<10
print(r.count) // 10
print(r[r.startIndex]) // 0
这里并没有一个数组在内存里存 0,1,2,...,9;Range 只是用起点、终点描述区间,按需算出元素。它更像区间视图,不是传统意义上的数组那种容器。
本文使用 文章同步助手 同步
从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI -- 肘子的 Swift 周报 #132
![]()
从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI
从 2019 年问世算起,SwiftUI 已经快七年了。它早已脱去了最初几年的稚气,逐渐成为苹果生态开发者的基础能力之一。不过,SwiftUI 的闭源属性也意味着,它的很多运行机制始终不透明。开发者在使用时固然能感受到它的表达优势,但一旦遇到问题,往往很难进一步追踪原因。这种特性也让 SwiftUI 在 AI 辅助编程时代显得有些“吃亏”——相比那些长期暴露在社区讨论、源码和文档中的技术,大模型能参考的高质量材料终究有限。
也正因此,社区一直希望通过开源项目去复刻 SwiftUI:一方面,是希望让 SwiftUI 这套优秀的设计有机会运行在更多平台上;另一方面,也是希望借助复刻过程,对 SwiftUI 的内部机制获得更多理解。最近几年,这方面最受关注的项目无疑是 OpenSwiftUI。在社区持续推进下,它已经补齐了 SwiftUI 的一部分核心实现,并在苹果生态之外的平台上做出了一些实验性探索。虽然距离它的目标显然还有不短的路要走,但它依然是当下开发者理解 SwiftUI 内部机制的重要入口之一。
其实,除了社区之外,一些公司,甚至规模很大的公司,也在过去几年里做过对 SwiftUI 的深入研究和复刻。上周,字节跳动开源了他们的 SwiftUI 复刻项目 DanceUI。
我第一次听说这个项目是在 2022 年。当时最让我感到意外的,不是“有人在复刻 SwiftUI”,而是“为什么是字节跳动在做这件事”。后来陆续和参与这个项目的开发者交流后,我大致理解了他们的动机:一方面,他们希望在将声明式开发引入庞大产品体系时获得更强的控制力;另一方面,也希望借由对 SwiftUI 这类优秀框架的研究,把运行时、依赖图和宿主整合等关键能力握在自己手里。和 OpenSwiftUI 相比,DanceUI 更不像一个社区式复刻项目,而更像一套从工程落地出发、反向拆解 SwiftUI 的样本。
更重要的是,过去几年中,DanceUI 已经在字节内部的一些产品模块中进入了生产环境。这意味着它显然不只是一个实验性的玩具,而是一套在性能和稳定性上都经受过一定检验的开发工具。对于 SwiftUI 开发者来说,它也因此提供了另一个理解 SwiftUI 的入口。
当然,这类项目并不适合被简单神化。它们不是 SwiftUI 本身,也不代表苹果官方实现。尤其像 OpenSwiftUI 这样带有强烈研究和兼容性导向的项目,本身就有明确边界;而像 DanceUI 这样的项目,则带着明显的大厂内部工程背景和落地取向。它们都不应该被当成“SwiftUI 真相”的唯一来源。
但这并不妨碍它们成为很好的学习材料。它们都不是 SwiftUI,却都能帮助我们更接近 SwiftUI。跟着开源项目去 dive SwiftUI,本质上不是在找一个“开源替代品”,而是在借这些项目训练自己理解 SwiftUI 的方式。
近期推荐
别让协议变成“怪物”:iOS 中的接口隔离实践 (Interface Segregation Principle In IOS: How To Prevent A Protocol From Becoming A Prison)
很多开发者可能都经历过类似的过程:项目早期一个精心设计的小协议,随着团队协作与业务演进,逐渐膨胀为难以维护的“怪物”。Pawel Kozielecki 通过一个逐步失控的 UserService 案例,具体展示了胖协议如何在团队协作中引入测试负担、隐性耦合,以及难以推进的重构成本。作者不仅给出了基于小协议组合与渐进迁移的现实方案,也点出了问题的根源:真正危险的,往往不是一次明显的设计失误,而是一连串“这次先加进去也没关系”的合理决定。
在 AI 辅助编程日益普及的背景下,这一问题反而更容易被放大。大模型倾向于依据文件名、协议名进行语义推断,一个模糊或过于宽泛的命名,往往会自然地吸引更多“不那么相关”的职责被不断叠加进去。清晰、准确且克制的命名,正在从代码风格问题,逐渐演变为影响系统边界的重要因素。
为 Text 实现删除线动画 (Animating Strikethroughs in SwiftUI)
为 SwiftUI Text 的删除线或下划线实现动画效果?不少人第一反应可能是基于 overlay + Shape 的方案。不过,这种方式很难正确适配 Dynamic Type 以及多行文本场景。Ashli Rankin 展示了一条更“系统化”的路径:基于 iOS 17 引入的 TextRenderer,直接访问 Text.Layout 的内部结构(行、glyph 等),并通过一个 progress 值在所有行之间累计绘制,从而实现连续、可动画的删除线效果。同时通过实现 Animatable,让 SwiftUI 在状态变化时自动完成插值过渡。
一个更有意思的细节在于:TextField 并不会走 Text 的渲染流程,因此 TextRenderer 无法直接应用。作者通过叠加一个透明的 Text(负责绘制动画)与真实的 TextField,并结合自定义 Layout 强制两者使用一致的换行宽度,最终解决了多行错位问题。
在 SwiftUI 预览中验证可访问性 (Checking accessibility with SwiftUI Previews)
SwiftUI Previews 通常用于检查界面布局,但同样可以在开发阶段快速验证部分可访问性(Accessibility)表现。Rob Whitake 梳理了几种常用途径:例如通过 Xcode Canvas 直接切换深浅色、方向、Dynamic Type 等进行快速检查,或借助 Preview Traits 定义特定的预览环境。文章还提到了一些仅用于 Preview 的私有环境变量(如增强对比度、减少动画、颜色反转等),通过带下划线的 keyPath 可以强制开启这些状态。不过需要注意,这类 API 必须限制在 #if DEBUG 中使用,以避免私有符号进入最终构建,带来审核风险。
一个 UIKit 项目的 SwiftUI 迁移实录
Yusuke Hosonuma 回顾了自己参与一个 UIKit + RxSwift + Coordinator 项目,并在一年多时间里逐步完成大部分界面 SwiftUI 化的经历。文章聚焦于真实项目中的工程取舍:在小团队、低沟通、几乎无文档的条件下,如何通过持续交付、渐进替换与尽量简单的设计,让项目保持可演进性。作者对不少常见做法都给出了很有现实感的反思,例如谨慎对待 protocol 抽象、EnvironmentObject、过早共通化,以及“顺手清理一切旧架构”的冲动。这并非单纯的技术实现总结,而是一篇充满真实感的团队实践复盘。
如何停止一个运行中的 SwiftUI 动画 Cancelling SwiftUI Animations: What Actually Works (And Why)
在 SwiftUI 中,停止一个已经运行的 repeatForever 动画并不像想象中那么简单。无论是使用 .none,还是通过 Transaction 禁用动画,都只能影响新的动画,而无法中断已经存在于渲染系统中的动画。Codelaby 给出了一个可行方案:通过自定义 CustomAnimation,让 animate 返回 nil(表示立即完成),并通过 shouldMerge 接管当前动画,从而实现终止动画的效果。
SwiftUI 会基于状态变化与动画函数自动进行插值计算。所谓“停止”,本质上是用一个新的状态变化去接管当前动画,而不是中断之前的动画。
工具
Swift Institute: 一个人的 Swift 基础设施重写
偶然看到的一个让我震惊的项目。Coen ten Thije Boonkkamp 在过去 9 个月里提交了约 9800 次 git commit,独自构建了一个分为 primitives、standards、foundations 三层、累计近 300 个包的 Swift 生态。目标只有一个——落地他去年提出的 Modern Swift Library Architecture 思想:依赖只能向下、集成发生在核心类型之外、"test what you own, trust what you import"。
一个人、一个构想,通过 AI 来进行尝试、验证。无论最后是否成功,但这是我想看到的 AI 意义。
swift-ast-lint:用 Swift 写 Swift 代码检查规则
由 Ryu 开发的 swift-ast-lint 不是另一个 SwiftLint,而是一套基于 SwiftSyntax 的自定义 lint 基础设施。它更适合需要编写 AST 级规则的团队,用来补足正则匹配在结构化检查上的局限。
项目支持脚手架生成、参数化规则、路径过滤以及 --fix 自动修复,比较适合处理架构约束、代码组织、模块边界等 regex 很难可靠覆盖的问题。它不太适合只想开箱即用的用户,但对于已经有明确工程规范、又希望把这些规范工具化的 Swift 团队来说,是一个值得关注的项目。
在 AI 辅助开发越来越普遍之后,真正有价值的可能不只是生成能力本身,还包括如何把团队规范和结构约束工具化。
活动
Swift Craft 2026
Swift Craft 是一个由社区驱动的 iOS / Apple 平台开发者大会,将于 5 月 18–20 日在英国 Folkestone 举行。目前议程已经公布,涵盖 Swift、SwiftUI 以及应用架构等多个方向。
相比大型会议,Swift Craft 更偏向小规模与深度交流,也更强调开发者之间的社区氛围。一个有趣的细节是本次会议的场地:位于海边悬崖上的 Leas Cliff Hall,会场三面落地窗直面英吉利海峡,这种环境本身就足以让会议体验变得与众不同。
主办方为本周报读者提供了折扣码 FBM26(£50 off Indie 票) 。如果你有参与线下开发者活动的计划,可以通过 Swift Craft tickets page 了解详情。
往期内容
💝 支持与反馈
如果本期周报对你有帮助,请:
- 👍 点赞 - 让更多开发者看到
- 💬 评论 - 分享你的看法或问题
- 🔄 转发 - 帮助同行共同成长
🚀 拓展 Swift 视野
- 📮 邮件订阅 | weekly.fatbobman.com 获取独家技术洞察
- 👥 开发者社区 | Discord 实时交流开发经验
- 📚 原创教程 | fatbobman.com 学习 Swift/SwiftUI 最佳实践
Swift 方法派发机制深度解析 —— 兼与 Objective-C `objc_msgSend` 对比
基于 Swift 5.10(含部分 Swift 6 行为)与现代 Objective-C(ARC + LLVM clang)。 文中代码片段为最小化示例,仅作为论点的佐证。
核心要点
| 派发方式 | 调用开销 | 触发条件 | 可被 Hook | 典型场景 |
|---|---|---|---|---|
| Static Dispatch(直接派发) | 最低,可内联 |
struct/enum 方法、final、全局函数、@inlinable
|
否 | 值类型、性能敏感路径 |
| V-Table Dispatch(虚表派发) | 一次间接跳转 |
class 的非 final 方法(无 @objc) |
否 | 普通 Swift 类继承 |
| Witness Table Dispatch | 一次表查 + 一次间接跳转 | 通过协议变量调用协议方法 | 否 | 面向协议编程 |
Message Dispatch(OC objc_msgSend) |
SEL→IMP 查表(带缓存) |
@objc dynamic、继承自 NSObject 且未优化 |
是(Swizzle/KVO) | OC 互操作、AOP |
一句话总结:Swift 默认追求"能静态就静态",只在继承、协议、互操作三条路径上才退化为表派发或消息派发;OC 则把所有方法调用统一成 objc_msgSend 的动态消息查找,灵活但每次调用都要付出查表代价。
1. 为什么要谈"派发"
方法派发(method dispatch)就是编译器/运行时如何把"调用某方法"翻译成"跳转到某段机器指令"。
派发方式直接决定三件事:
- 性能:是否能内联、是否要查表、是否能命中分支预测。
- 可扩展性:能不能在运行时替换实现(Swizzle、KVO、Mock)。
- 二进制兼容:库的方法表布局变化是否会破坏调用方。
OC 与 Swift 在这三个维度上的设计哲学截然相反,下面分别拆解。
2. Objective-C:一切皆消息
2.1 objc_msgSend 的本质
OC 中 [obj doSomething:arg] 在编译期不会被解析为某个具体函数地址,而是被翻译成:
((void (*)(id, SEL, id))objc_msgSend)(obj, @selector(doSomething:), arg);
objc_msgSend 是一段手写汇编,做的事情大致是:
1. 取 obj->isa 拿到 Class
2. 在 Class 的 method cache 里按 SEL hash 查 IMP
3. 命中 → 直接 jmp IMP(尾调用,不入栈)
4. 未命中 → __class_lookupMethodAndLoadCache3 走 method list / 父类链
5. 查到 → 写回 cache
6. 仍找不到 → 进入 forwarding(resolveInstanceMethod / forwardingTargetForSelector / forwardInvocation)
⚠️ 实战提示:
objc_msgSend的 cache 是 per-class 的开放寻址哈希表,命中率通常 > 95%。这意味着"OC 方法慢"的直觉并不准确——在热路径上一次调用通常只多花几个时钟周期,远低于一次 cache miss。真正的成本不在派发本身,而在无法内联导致优化器失去全局视野。
2.2 消息派发带来的能力
消息派发让以下能力成为零成本默认值:
-
Method Swizzling:替换
Class的 method list 即可全局劫持。 -
KVO:runtime 动态生成
NSKVONotifying_XXX子类并替换isa。 -
响应链 / Target-Action:
UIApplication sendAction:to:from:forEvent:完全建立在 SEL 之上。 -
消息转发:
forwardInvocation:让一个对象"假装"实现某协议,是 OC 多代理与 RPC 框架的基石。
代价是:编译期不知道 IMP 是什么,全程无法内联,也无法做跨方法优化。
3. Swift:四种派发方式共存
Swift 没有"统一派发"的设计,而是根据声明上下文挑选最便宜的合法方式。理解 Swift 性能模型的关键,就是搞清"什么场景用哪种"。
3.1 Static Dispatch(直接派发)
调用直接编译成 call <symbol>,可被内联、可被常量折叠。触发条件:
-
struct、enum的所有方法(值类型不存在继承) -
class中标了final的方法、或final class的全部方法 -
private方法(编译器能证明无覆写) - 全局函数、
static函数 -
@inlinable/@_transparent修饰的方法
struct Counter {
var value = 0
mutating func tick() { value += 1 }
}
var c = Counter()
c.tick()
c.tick() 在 -O 下会被完全内联,最终汇编里看不到调用指令,只剩一条 add。
3.2 V-Table Dispatch(虚表派发)
Swift 的 class 与 C++ 类似,每个类有一张 V-Table(在 metadata 末尾),按声明顺序存放方法指针。调用时:
1. 从 obj 的 metadata 偏移取 V-Table
2. 按方法在表中的固定 index 取 IMP
3. call IMP
只有一次表查 + 一次间接跳转,比 objc_msgSend 少了 SEL hash 与 cache 命中判断,但因为是间接调用,仍然无法跨方法内联。
class Animal {
func speak() { print("...") }
}
final class Dog: Animal {
override func speak() { print("woof") }
}
Animal 的 speak 通过 V-Table 派发;Dog 因为 final,其调用点会被去虚化(devirtualize)回 static dispatch。
3.3 Witness Table Dispatch(协议见证表)
通过协议变量调用协议方法时,Swift 用一张 Protocol Witness Table(PWT):每个"类型 × 协议"对生成一张表,表里按协议方法的固定 index 存放该类型的具体实现。
protocol Drawable {
func draw()
}
struct Circle: Drawable { func draw() { /* ... */ } }
func render(_ d: Drawable) {
d.draw()
}
render 拿到的 d 是一个 existential container(值 + 类型 metadata + PWT 指针)。d.draw() 实际是:
1. 从 existential container 取 PWT
2. 按 draw 在协议中的 index 取 witness
3. call witness(witness 内部再 call 真正的实现)
⚠️ 实战坑:
some Drawable(opaque return type)和Drawable(existential)的派发完全不同。前者编译期就确定了具体类型,可以走 static dispatch + 单态化(specialization),后者必须走 PWT。把热路径里的func make() -> Drawable改成func make() -> some Drawable,在 SwiftUI / 集合操作里能拿到数量级的性能提升。
3.4 Message Dispatch(走 objc_msgSend)
Swift 在以下两种情况会退化到 OC 的消息派发:
- 显式标注
@objc dynamic - 类继承自
NSObject,且方法满足@objc暴露规则,且没有被去虚化优化
class MyVC: UIViewController {
@objc dynamic func reload() { /* ... */ }
}
只有 @objc dynamic 的方法是保证走 objc_msgSend 的,因此也只有它能被 KVO / Method Swizzling 劫持。仅 @objc 不带 dynamic 的方法,编译器仍可能选择 V-Table 派发。
4. 派发规则速查表
把"声明位置 × 修饰符"组合起来,就是一张完整的决策表:
| 声明上下文 | 默认派发 | 加 final
|
加 @objc
|
加 @objc dynamic
|
|---|---|---|---|---|
struct / enum 方法 |
Static | — | 不允许 | 不允许 |
class 直接定义的方法 |
V-Table | Static | V-Table(兼可 OC 调) | Message |
class extension 中的方法 |
Static | Static | V-Table | Message |
protocol 要求的方法 |
Witness | — | Message(要求 @objc protocol) |
Message |
protocol extension 默认实现 |
Static | — | 不允许 | 不允许 |
NSObject 子类的方法 |
V-Table | Static | V-Table | Message |
几条容易踩的经验法则:
-
extension中定义的方法即使被子类重写也不会触发动态派发,重写会被静默忽略。要支持覆写就把方法定义放回类主体。 - 协议 extension 的"默认实现"是 static 的,不会走 PWT。如果某个类型实现了同名方法,但调用方持有的是协议变量,仍可能调到 default 实现(这是经典面试题)。
-
@objc≠dynamic:前者只是给 OC runtime 暴露符号,后者才强制走消息派发。
5. 性能:到底差多少
简化的相对开销(命中 cache、无优化干扰的情况下):
| 派发方式 | 相对开销 | 备注 |
|---|---|---|
| Inlined static | ~1× | 实质上没有调用 |
| Direct call (static) | ~1× | 一条 call
|
| V-Table | ~1.5–2× | 一次 load + 间接 call |
| Witness Table | ~2× | 与 V-Table 量级相同 |
objc_msgSend(cache 命中) |
~3–5× | 多了 SEL hash 与 cache 比对 |
objc_msgSend(cache miss) |
数十× | 走 method list 查找 |
但真实业务里这些差异通常被淹没,例如 UI 主线程一次 reloadData 的耗时可能是 10ms 量级,几百次 objc_msgSend 完全可以忽略。性能差异真正显著的场景是:
-
集合的内层热循环(
map/filter/ 自定义 reduce) -
每帧调用的渲染回调(
CADisplayLink、SwiftUI 的body求值) - 大量小对象的属性 getter/setter(特别是泛型容器)
6. 选型与最佳实践
6.1 写 Swift 类型时
- 默认优先
struct,需要引用语义或 OC 互操作再用class。 -
class不需要继承时直接final class,让编译器去虚化。 - 协议返回值能用
some P就别用P,能用any P就别忘加any让代码意图清晰。 - 性能敏感的 ABI 稳定库导出 API 时配合
@inlinable+@usableFromInline。
6.2 需要动态能力时
- 要被 KVO 监听 →
@objc dynamic var ... - 要被 Swizzle / Aspect →
@objc dynamic func ... - 要在 OC 代码里调用 →
@objc(不必加dynamic) - 要做 Mock / Stub → 优先用协议依赖注入,而不是 Swizzle
6.3 OC 仍不可替代的场景
公平起见,OC 的消息派发模型在以下场景仍有 Swift 难以替代的优势:
| 维度 | OC 占优的原因 |
|---|---|
| 编译速度 | 没有泛型单态化、类型推断爆炸,增量编译显著快于 Swift |
| 运行时反射 |
class_copyMethodList / class_copyIvarList 等一整套 runtime API |
| 二进制体积 | 没有 witness table / metadata 膨胀,纯 OC 类的 metadata 极小 |
| AOP / Hook 生态 | Aspect、Stinger、KVO 都建立在 objc_msgSend 之上,Swift 难以等价替代 |
| C / C++ 互操作 | 与 C 二进制接口零成本互通 |
工程实践中,底层基础库(埋点、网络、监控)继续用 OC,业务层迁 Swift,往往是兼顾性能与生产力的最稳妥分工。
7. 一个综合案例
下面这段代码同时触发了四种派发,是理解 Swift 派发模型的一个好对照:
@objc protocol Refreshable { func refresh() }
class Base: NSObject, Refreshable {
func refresh() { print("base") }
}
final class Leaf: Base {
override func refresh() { print("leaf") }
}
let a: Refreshable = Leaf()
let b: Base = Leaf()
let c: Leaf = Leaf()
a.refresh()
b.refresh()
c.refresh()
-
a.refresh():Refreshable是@objc protocol,走objc_msgSend。 -
b.refresh():Base继承NSObject,编译器保守起见走 V-Table(若Base也是final,可去虚化)。 -
c.refresh():Leaf是final,编译器去虚化为 Static,可被内联。
把同一个方法在三种持有方式下分别调用,得到三种不同的派发路径——这是 OC 永远不会出现的现象,也是 Swift 性能调优最容易被忽略的细节。
从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI - 肘子的 Swift 周报 #132
从 2019 年问世算起,SwiftUI 已经快七年了。它早已脱去了最初几年的稚气,逐渐成为苹果生态开发者的基础能力之一。不过,SwiftUI 的闭源属性也意味着,它的很多运行机制始终不透明。开发者在使用时固然能感受到它的表达优势,但一旦遇到问题,往往很难进一步追踪原因。这种特性也让 SwiftUI 在 AI 辅助编程时代显得有些“吃亏”——相比那些长期暴露在社区讨论、源码和文档中的技术,大模型能参考的高质量材料终究有限。
【SwiftyJSON】拯救你的 as? [String: Any]——链式 JSON 访问的正确姿势
【SwiftyJSON】拯救你的 as? [String: Any]——链式 JSON 访问的正确姿势
iOS三方库精读 · 第 15 期
一、一句话介绍
SwiftyJSON 是一个用于 iOS/macOS 的 JSON 解析辅助库,它通过链式下标访问和安全类型转换,让原本需要大量 as? 强转和 guard let 解包的 JSON 解析代码,变成像访问字典一样直观的单行操作。
| 属性 | 信息 |
|---|---|
| ⭐ GitHub Stars | 22k+ |
| 最新稳定版 | 5.0.2 |
| License | MIT |
| 支持平台 | iOS 13+ / macOS 11+ |
| 语言 | Swift(纯 Swift,无 OC 接口) |
二、为什么选择它
原生痛点
原生 JSONSerialization 解析复杂 JSON 的体验:
// ❌ 原生方式:每层都要 as? + guard,代码量爆炸
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let user = json["user"] as? [String: Any],
let profile = user["profile"] as? [String: Any],
let bio = profile["bio"] as? String,
let score = profile["score"] as? Double else {
return
}
5 层嵌套,1 个字段。如果某层返回 null,整个 guard 失败,无法优雅降级。
SwiftyJSON 方式:
// ✅ SwiftyJSON:一行,安全,不崩溃
let bio = json["user"]["profile"]["bio"].stringValue // 不存在则 ""
let score = json["user"]["profile"]["score"].doubleValue // 不存在则 0.0
核心优势:
- 链式下标:无论嵌套多深,中间路径不存在也不崩溃
-
类型转换属性:
.stringValue/.intValue/.boolValue自动转换 + 默认值 -
Optional 版本:
.string/.int/.bool返回Optional,可if let -
数组/字典直接遍历:
.arrayValue/.dictionaryValue -
null 安全:
.isNull和.exists()区分"不存在"和"存在但为 null"
三、核心功能速览
基础层(新手必读)
环境集成
// SPM
// URL: https://github.com/SwiftyJSON/SwiftyJSON.git
// from: "5.0.2"
# CocoaPods
pod 'SwiftyJSON', '~> 5.0'
创建 JSON 对象
import SwiftyJSON
// 从 Data 创建(最常用)
let json = JSON(data)
// 从字典/数组创建
let json2 = JSON(["name": "Alice", "age": 25])
// 从字符串创建
let json3 = JSON(parseJSON: "{\"key\": \"value\"}")
值访问:xValue vs x(Optional)
// .stringValue → String(不存在时返回 "")
// .string → String?(不存在时返回 nil)
let name1 = json["user"]["name"].stringValue // "Alice" 或 ""
let name2 = json["user"]["name"].string // "Alice" 或 nil
// 其他类型同理
json["count"].intValue // Int,默认 0
json["count"].int // Int?
json["score"].doubleValue // Double,默认 0.0
json["score"].double // Double?
json["active"].boolValue // Bool,默认 false
json["active"].bool // Bool?
进阶层(最佳实践)
数组遍历
// arrayValue: 返回 [JSON],安全(不存在返回 [])
for item in json["user"]["repos"].arrayValue {
let name = item["name"].stringValue
let stars = item["stars"].intValue
print("\(name): ⭐ \(stars)")
}
// 快速 map
let repoNames = json["user"]["repos"].arrayValue
.map { $0["name"].stringValue }
.filter { !$0.isEmpty }
字典遍历
// dictionaryValue: 返回 [String: JSON]
for (key, value) in json["user"]["metadata"].dictionaryValue {
print("\(key): \(value)")
}
Null 处理
let field = json["user"]["lastLogin"]
// 区分"不存在"和"存在但为 null"
print(field.exists()) // false → 路径不存在
print(field.isNull) // true → 路径不存在或值为 null
// 带默认值的安全访问
let last = json["user"]["lastLogin"].string ?? "从未登录"
整数索引访问数组
let firstTag = json["user"]["tags"][0].stringValue // "swift"
let lastRepo = json["user"]["repos"][2]["name"].stringValue // "TodoApp"
SwiftyJSON 转回 Data / 字典
// 转回 Data(用于 Codable 混用)
let rawData = try? json["user"]["repos"].rawData()
// 转回 [String: Any]
let rawDict = json.dictionaryObject // [String: Any]?
let rawArr = json.arrayObject // [Any]?
与 Codable 混用(最佳实践)
// 用 SwiftyJSON 做"柔性"部分,Codable 做"结构化"部分
let json = JSON(data)
// 1. 取出子 JSON(SwiftyJSON 处理不确定的动态结构)
let extraInfo = json["response"]["extra"] // 动态字段,结构不定
// 2. 将确定结构的部分转为 Codable
if let reposData = try? json["user"]["repos"].rawData() {
let repos = try? JSONDecoder().decode([Repo].self, from: reposData)
}
深入层(源码视角)
JSON 的枚举本质
SwiftyJSON 的核心是一个 JSON 结构体,内部用枚举表示类型:
public struct JSON {
// 内部存储联合类型
fileprivate var rawArray: [Any] = []
fileprivate var rawDictionary: [String: Any] = [:]
fileprivate var rawString: String = ""
fileprivate var rawNumber: NSNumber = 0
fileprivate var rawNull: NSNull = NSNull()
fileprivate var rawBool: Bool = false
public internal(set) var type: Type = .null
}
subscript 访问时,如果类型不匹配或 key 不存在,返回一个 JSON.null 单例而非崩溃。这是链式访问安全性的核心保障。
性能注意
每次 subscript 访问都会创建新的 JSON 实例(值类型复制),深层链式访问在循环中可能造成性能开销。热路径代码建议:
// ❌ 在循环中重复深层访问
for _ in 0..<10000 {
let _ = json["a"]["b"]["c"]["d"].stringValue
}
// ✅ 缓存中间节点
let profile = json["a"]["b"] // 只创建一次
for _ in 0..<10000 {
let _ = profile["c"]["d"].stringValue
}
四、实战演示
场景:解析 GitHub API 响应
// 解析 https://api.github.com/search/repositories?q=swift 的响应
func parseSearchResult(data: Data) -> [String] {
let json = JSON(data)
// 总数
let total = json["total_count"].intValue
print("找到 \(total) 个仓库")
// 取前 5 个仓库名
return json["items"].arrayValue.prefix(5).map { repo in
let name = repo["full_name"].stringValue
let stars = repo["stargazers_count"].intValue
let lang = repo["language"].string ?? "Unknown"
return "\(name) ⭐\(stars) [\(lang)]"
}
}
五、源码亮点
进阶层:链式安全的实现
// SwiftyJSON 的 subscript 关键实现
public subscript(key: String) -> JSON {
get {
if type == .dictionary {
if let value = rawDictionary[key] {
return JSON(value)
}
}
return JSON.null // ← 不崩溃,返回 null JSON
}
}
JSON.null 是一个静态单例,所有对它的 subscript 访问都继续返回自身,形成"null 传播链",这就是为什么 json["a"]["b"]["c"]["d"] 即便 "a" 不存在也不会 crash。
深入层:与 Codable 的本质区别
| 维度 | SwiftyJSON | Codable |
|---|---|---|
| 解析时机 | 运行时,按需访问 | 解码时一次性反序列化 |
| 类型错误 | 运行时,返回默认值 | 编译时 / 解码时抛错 |
| 内存占用 | 保留完整 JSON 树 | 只保留 struct/class 数据 |
| 适用场景 | 探索、动态结构 | 固定 API 模型 |
六、踩坑记录
问题 1:.string 返回 nil 而 .stringValue 返回空字符串
-
原因:JSON 中该字段是
null或类型是 Number,.string只在类型是 String 时返回非 nil -
解决:根据场景选择:
string ?? "默认值"或.stringValue;如果需要 Number → String 转换:let val = json["count"].string ?? json["count"].numberValue.stringValue
问题 2:修改 SwiftyJSON 的值没有生效
-
原因:
JSON是值类型(struct),赋值后修改的是副本 -
解决:
var json = JSON(data) json["user"]["name"] = "New Name" // ✅ 使用 subscript setter
问题 3:Swift Package Manager 找不到模块
-
原因:SwiftyJSON 的 SPM 包名是
SwiftyJSON,但有时大小写不一致 -
解决:确保
import SwiftyJSON(大驼峰),检查 SPM 依赖是否成功解析
问题 4:OC 项目无法使用 SwiftyJSON
- 原因:SwiftyJSON 是纯 Swift,OC 不能直接 import
-
解决:OC 项目用
NSJSONSerialization+ YYModel / MJExtension,或在 Swift 桥接层封装
问题 5:解析性能在大量数据时较差
-
原因:SwiftyJSON 在内部创建大量临时
JSON实例 -
解决:大量数据(10w+ 条)时改用
Codable,小量动态数据 SwiftyJSON 够用
七、延伸思考
JSON 解析方案全景对比
| 方案 | 类型安全 | 动态 JSON | OC 支持 | 性能 | 推荐场景 |
|---|---|---|---|---|---|
| SwiftyJSON | 运行时 | ✅ 最好 | ❌ | 中等 | 探索/动态结构 |
| Codable | 编译时 | ⚠️ 需 AnyCodable | ❌ | 高 | 固定 API 模型 |
| YYModel (OC) | 运行时 | ✅ | ✅ | 高 | OC 项目 |
| ObjectMapper | 运行时 | ✅ | ❌ | 中等 | Swift,已有项目 |
| NSJSONSerialization | 无 | ✅ | ✅ | 高 | 简单/OC 场景 |
推荐原则
新项目 Swift:优先
Codable,复杂动态 JSON 用 SwiftyJSON 辅助。 老项目 OC:NSJSONSerialization+ YYModel / MJExtension。 混合项目:在 Swift 层用 Codable 建模,可选 SwiftyJSON 处理边界情况。
八、参考资源
- GitHub: SwiftyJSON/SwiftyJSON
- Apple Codable 文档
- YYModel(OC 模型框架)
- WWDC 2017 Session 212 - What's New in Foundation
- 系列 Demo 仓库:
github.com/yourname/ios-lib-demos
九、本期互动
小作业
用 SwiftyJSON 解析一个真实 API(如 GitHub / 豆瓣 / OpenWeather),要求:处理嵌套 3 层以上的 JSON,包含数组遍历和 null 字段处理,最终展示在 UITableView 中。评论区分享你选的 API 和最复杂的解析路径。
思考题
SwiftyJSON 用"null 传播"(路径不存在时返回 JSON.null 而非 crash)来保证安全性,而 Swift Codable 用 Optional 和 throws 来保证类型安全。这两种设计哲学各有什么权衡?在什么情况下"静默返回默认值"比"抛出错误"更合适?
读者征集
下一期我们将深入 R.swift(编译时安全的资源访问)。你在项目中遇到过资源文件名拼写错误导致运行时崩溃吗?你目前是如何管理图片/字体/颜色等资源的?欢迎评论区分享你的资源管理方案。
📅 本系列每周五晚更新 ✅ 第11期:DGCharts · ✅ 第12期:Hero · ✅ 第13期:Realm · ✅ 第14期:Moya · ➡️ 第15期:SwiftyJSON · ○ 第16期:R.swift
【Moya】为什么你的 Alamofire 代码需要再封装一层?
【Moya】为什么你的 Alamofire 代码需要再封装一层?
iOS三方库精读 · 第 14 期
一、一句话介绍
Moya 是一个建立在 Alamofire 之上的网络抽象层库,它用 TargetType 协议将所有 API 接口声明为 Swift 枚举 case,让网络请求从"散落在各处的字符串 URL"变成"编译器可检查的类型化接口",同时内置单元测试 Stubbing 和 Plugin 拦截机制。
| 属性 | 信息 |
|---|---|
| ⭐ GitHub Stars | 15k+ |
| 最新稳定版 | 15.0.3 |
| License | MIT |
| 支持平台 | iOS 13+ |
| 语言 | Swift(纯 Swift,无 OC 接口) |
| 依赖 | Alamofire 5.x |
二、为什么选择它
原生痛点
直接使用 Alamofire 或 URLSession 时,常见的问题:
// ❌ 硬编码 URL,散落在各处
AF.request("https://api.example.com/users/\(userId)/profile",
method: .get,
parameters: ["include": "avatar"],
headers: ["Authorization": "Bearer \(token)"])
- URL 字符串:运行时才发现拼写错误
-
参数类型:
parameters: [String: Any],传错类型编译器不报错 - 认证 Token:每个请求都要手动加 headers
- Mock 测试:需要替换整个 URLSession,实现复杂
- 接口文档:散落在业务代码里,难以统一维护
Moya 的解决方案:
- TargetType 协议:将每个 API 的 URL、方法、参数、headers 集中声明
- 类型安全:枚举 case 关联值确保参数类型正确,编译时报错
- Plugin 系统:一处注入 Token,所有请求自动携带
-
内置 Stubbing:
sampleData+StubbingProvider,无需 mock URLSession
三、核心功能速览
基础层(新手必读)
环境集成
// SPM
// URL: https://github.com/Moya/Moya.git
// from: "15.0.3"
// Products: Moya(基础)/ RxMoya(RxSwift)/ CombineMoya(Combine)
# CocoaPods
pod 'Moya', '~> 15.0'
pod 'Moya/RxSwift' # 可选
pod 'Moya/Combine' # 可选
TargetType 完整示例
import Moya
enum UserAPI {
case login(email: String, password: String)
case profile(userId: Int)
case updateAvatar(data: Data)
case logout
}
extension UserAPI: TargetType {
var baseURL: URL { URL(string: "https://api.example.com/v2")! }
var path: String {
switch self {
case .login: return "/auth/login"
case .profile(let id): return "/users/\(id)"
case .updateAvatar: return "/users/avatar"
case .logout: return "/auth/logout"
}
}
var method: Moya.Method {
switch self {
case .login, .updateAvatar: return .post
case .logout: return .delete
case .profile: return .get
}
}
var task: Task {
switch self {
case .login(let email, let pw):
return .requestParameters(
parameters: ["email": email, "password": pw],
encoding: JSONEncoding.default
)
case .updateAvatar(let data):
let formData = MultipartFormData(provider: .data(data),
name: "file",
fileName: "avatar.jpg",
mimeType: "image/jpeg")
return .uploadMultipart([formData])
default:
return .requestPlain
}
}
var headers: [String: String]? { ["Content-Type": "application/json"] }
// 单元测试用的 Stub 数据
var sampleData: Data {
switch self {
case .login:
return """{"token":"test_token","userId":1}""".data(using: .utf8)!
default:
return Data()
}
}
}
发起请求
let provider = MoyaProvider<UserAPI>()
// 回调方式
provider.request(.login(email: "test@example.com", password: "123456")) { result in
switch result {
case .success(let response):
let json = try? response.mapJSON()
print(json ?? "")
case .failure(let error):
print(error)
}
}
// 直接 map 到 Codable 模型
provider.request(.profile(userId: 42)) { result in
if case .success(let response) = result {
let user = try? response.map(User.self)
}
}
进阶层(最佳实践)
Plugin 系统:拦截所有请求
// 统一注入认证 Token
struct TokenPlugin: PluginType {
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
var req = request
if let token = AuthManager.shared.token {
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return req
}
// 401 自动触发 token 刷新
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
if case .success(let response) = result, response.statusCode == 401 {
AuthManager.shared.refreshToken()
}
}
}
// 统计 API 耗时
struct MetricsPlugin: PluginType {
func willSend(_ request: RequestType, target: TargetType) {
Analytics.trackStart(api: target.path)
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
Analytics.trackEnd(api: target.path)
}
}
// 组合多个 Plugin
let provider = MoyaProvider<UserAPI>(plugins: [
TokenPlugin(),
MetricsPlugin(),
NetworkLoggerPlugin() // Moya 内置日志插件
])
单元测试:Stubbing
// 无需真实网络,立即返回 sampleData
let testProvider = MoyaProvider<UserAPI>(stubClosure: MoyaProvider.immediatelyStub)
// 延迟返回(模拟网络延迟)
let testProvider2 = MoyaProvider<UserAPI>(
stubClosure: MoyaProvider.delayedStub(0.5) // 延迟 0.5s
)
// 测试代码
func testLogin() {
testProvider.request(.login(email: "test@example.com", password: "123")) { result in
switch result {
case .success(let response):
XCTAssertEqual(response.statusCode, 200)
let model = try? response.map(LoginResponse.self)
XCTAssertNotNil(model?.token)
case .failure:
XCTFail()
}
}
}
Combine 集成
import Combine
class UserViewModel: ObservableObject {
@Published var user: User?
@Published var error: String?
private var cancellables = Set<AnyCancellable>()
private let provider = MoyaProvider<UserAPI>()
func loadProfile(userId: Int) {
provider.requestPublisher(.profile(userId: userId))
.tryMap { try $0.map(User.self) }
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
if case .failure(let error) = completion {
self?.error = error.localizedDescription
}
},
receiveValue: { [weak self] user in
self?.user = user
}
)
.store(in: &cancellables)
}
}
RxSwift 集成
import RxSwift
provider.rx.request(.searchRepos(query: "swift"))
.map(RepoSearchResult.self)
.observe(on: MainScheduler.instance)
.subscribe(
onSuccess: { result in print(result.items) },
onFailure: { error in print(error) }
)
.disposed(by: disposeBag)
深入层(源码视角)
MoyaProvider 的请求流程
provider.request(.login(...))
↓
MoyaProvider.requestNormal
↓
调用所有 Plugin.prepare(修改 URLRequest)
↓
调用 Plugin.willSend(发送前通知)
↓
Alamofire.request(真正发网络请求)
↓
收到响应
↓
调用 Plugin.didReceive(响应后通知)
↓
回调 completion handler
Moya 本质是 Alamofire 的装饰器(Decorator),所有的实际网络操作都委托给 Alamofire,Moya 只负责协议声明、Plugin 拦截、Stub 切换。
Task 枚举的设计
Task 枚举涵盖了所有常见请求类型:
public enum Task {
case requestPlain // 无 body
case requestData(_ data: Data) // 原始 Data
case requestParameters(parameters:encoding:) // URL 参数 or JSON body
case uploadMultipart(_ data: [MultipartFormData]) // 文件上传
case downloadDestination(_ destination: DownloadDestination) // 文件下载
case uploadCompositeMultipart(_, urlParameters:) // 混合上传
// ...
}
这种穷举枚举设计确保了所有请求形式都有类型安全的表达方式。
四、实战演示
场景:统一网络层封装(生产级模板)
// 1. 定义 API Target
enum NewsAPI {
case topHeadlines(country: String, page: Int)
case article(id: String)
}
extension NewsAPI: TargetType {
var baseURL: URL { URL(string: "https://newsapi.org/v2")! }
var path: String {
switch self {
case .topHeadlines: return "/top-headlines"
case .article(let id): return "/articles/\(id)"
}
}
var method: Moya.Method { .get }
var task: Task {
switch self {
case .topHeadlines(let country, let page):
return .requestParameters(
parameters: ["country": country, "page": page, "pageSize": 20],
encoding: URLEncoding.queryString
)
case .article: return .requestPlain
}
}
var headers: [String: String]? { nil }
var sampleData: Data { Data() }
}
// 2. Service 层封装(屏蔽 Moya 细节)
final class NewsService {
private let provider = MoyaProvider<NewsAPI>(plugins: [
TokenPlugin(),
NetworkLoggerPlugin()
])
func fetchHeadlines(country: String, page: Int) async throws -> [Article] {
return try await withCheckedThrowingContinuation { cont in
provider.request(.topHeadlines(country: country, page: page)) { result in
switch result {
case .success(let response):
do {
let articles = try response.map([Article].self, atKeyPath: "articles")
cont.resume(returning: articles)
} catch {
cont.resume(throwing: error)
}
case .failure(let error):
cont.resume(throwing: error)
}
}
}
}
}
// 3. 业务层调用
let service = NewsService()
Task {
let articles = try await service.fetchHeadlines(country: "cn", page: 1)
}
五、源码亮点
进阶层
TargetType 作为抽象屏障
Moya 的 MoyaProvider<Target: TargetType> 是泛型类型,每种 API 有独立的 Provider 实例。这意味着:
- 不同 API 服务(UserAPI / ProductAPI / OrderAPI)完全隔离
- 每个 Provider 可以配置不同的 Plugin(如不同的 Token 策略)
- 测试时替换 Provider 无需修改任何业务代码
深入层:网络层的 SOLID 原则
Moya 的设计完美体现了 SOLID 原则:
| 原则 | 体现 |
|---|---|
| Single Responsibility | TargetType 只描述接口声明,Provider 只负责执行 |
| Open/Closed | 新增 API 只需新增枚举 case,不修改 Provider |
| Liskov Substitution | StubbingProvider 可无缝替换真实 Provider |
| Interface Segregation | Plugin 协议的每个方法都是可选实现 |
| Dependency Inversion | 业务代码依赖 TargetType 协议,而非具体 URL 字符串 |
六、踩坑记录
问题 1:sampleData 返回空 Data 导致测试解析失败
-
原因:使用
StubbingProvider但忘记实现sampleData -
解决:为每个需要测试的 case 提供合法 JSON 的
sampleData
问题 2:Plugin.prepare 中修改 headers 无效
-
原因:
URLRequest是值类型,必须先 copy 再修改 -
解决:
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { var req = request // ← copy 一份 req.setValue("Bearer xxx", forHTTPHeaderField: "Authorization") return req // ← 返回修改后的副本 }
问题 3:Moya 请求不在主线程回调
-
原因:默认
callbackQueue是.main,但某些版本或配置下可能改变 -
解决:UI 更新前显式
DispatchQueue.main.async { ... }或使用.receive(on: MainScheduler.instance)
问题 4:多个 API 服务需要不同 baseURL
- 原因:Moya 一个 TargetType 对应一个 baseURL
-
解决:拆分为多个 enum(
UserAPI,ProductAPI),各自独立 Provider
问题 5:文件上传进度无法监听
- 原因:回调方式无进度回调
-
解决:
provider.request(.uploadAvatar(data: imageData)) { result in ... } // 上面不支持进度,改用: provider.requestWithProgress(.uploadAvatar(data: imageData)) { progress in print("上传进度:", progress.progress) }
七、延伸思考
同类方案对比
| 方案 | 类型安全 | 测试友好 | 学习成本 | OC 支持 | 推荐场景 |
|---|---|---|---|---|---|
| URLSession | ❌ 字符串 | 需 mock | 低 | ✅ | 超简单场景 |
| Alamofire | ⚠️ 需封装 | 需封装 | 低 | ❌ | 中型 App |
| Moya | ✅ 枚举 | ✅ 内置 | 中 | ❌ | 中大型 Swift App |
| Apollo(GraphQL) | ✅ 代码生成 | ✅ | 高 | ⚠️ | GraphQL API |
推荐使用场景
- ✅ API 接口较多(20+)的中大型 App
- ✅ 团队协作,需要统一的 API 文档化
- ✅ 需要完整的单元测试覆盖网络层
- ✅ 已经在使用 Alamofire 想升级架构
不推荐场景
- ❌ API 极少(3 个以内)→ 直接 Alamofire 更简单
- ❌ OC 项目 → Moya 纯 Swift,考虑 AFNetworking
- ❌ 追求最小依赖体积 → URLSession 直接封装
八、参考资源
- GitHub: Moya/Moya
- Moya 官方文档
- Moya 与 RxSwift 集成
- Moya 与 Combine 集成
- 系列 Demo 仓库:
github.com/yourname/ios-lib-demos
九、本期互动
小作业
用 Moya 封装一个天气 API 服务层:定义 WeatherAPI enum,包含"当前天气"和"5天预报"两个 case,实现 TargetType,并编写一个 TokenPlugin 注入 API Key,最后用 StubbingProvider 为两个接口各写一个单元测试。评论区分享你的 TargetType 实现。
思考题
Moya 的 TargetType 强制将一个服务的所有 API 放在同一个 enum 里。当 API 接口很多时(50+),这个大 enum 会变得难以维护。你会如何设计拆分方案?能否在不改变使用方代码的前提下实现"API 分模块管理"?
读者征集
下一期我们将深入 SwiftyJSON(JSON 解析利器)。你在处理复杂嵌套 JSON 时用过哪些方案(SwiftyJSON / Codable / ObjectMapper / 手动解析)?在 OC 项目中你是如何处理 JSON 的?欢迎评论区分享。
📅 本系列每周五晚更新 ✅ 第11期:DGCharts · ✅ 第12期:Hero · ✅ 第13期:Realm · ➡️ 第14期:Moya · ○ 第15期:SwiftyJSON