iOS 是高价值、强约束的平台:用户付费意愿强,但生态封闭、上架审核严。Swift 是苹果力推的现代语言,SwiftUI 用声明式语法把 UI 写得像搭积木。本页按完整知识链铺开:Swift 语言核心 → SwiftUI 声明式 UI → UIKit 与生命周期 → App 架构 → 持久化 → 打包与性能(Instruments)。我会像讲 Java 进阶那样,把每个知识点拆成"它是什么 → 为什么这样设计 → 怎么用 → 踩过什么坑"。基线:Swift 5.9+ / SwiftUI 5,开发需一台 Mac + Xcode。
Swift 语言:可选值、协议、泛型与并发
iOS 开发过去离不开 Objective-C(一门历史包袱很重的语言)。2014 年苹果推出 Swift,今天新项目主推 SwiftUI + Swift。这一章把 Swift 最核心、也最容易"自以为懂"的语言特性讲透:可选值、值类型、面向协议、泛型、结构化并发与 Actor。
可选值:用类型把"空"管起来
Swift 把"可能有值也可能没有"显式写成 String?。编译器逼你处理"空"的情况,把一大类崩溃移到编译期。
! 强解包
nick! 等于对编译器喊"它一定不为 nil",一旦真为空就直接崩溃(运行时陷阱)。只在你 100% 确定已赋值时用,比如 IBOutlet 或 guard let 之后的变量。能用 if let/guard let/?? 就别用 !。
值类型 struct / enum 与"无隐式共享"
Swift 默认推荐 struct(值类型)和 enum(同样是值类型)。赋值时是拷贝,不会出现"另一个地方偷偷改了我的数据"。
论为什么 Swift 偏爱 struct
是什么:struct 是值类型,赋值/传参时是拷贝;class 是引用类型,传的是同一块内存的"指针"。
为什么:值类型没有隐式共享,多线程下不会出现"另一个线程偷偷改了我的数据",天然更利于并发安全。苹果官方建议:默认用 struct,需要共享/继承才用 class。
协议与面向协议编程(POP)
protocol 定义能力契约,extension 还能给协议加默认实现——这就是"面向协议编程",比继承更灵活、可组合。
论为什么 POP 比继承香
Swift 单继承。想让"既能飞又能叫"的对象复用逻辑,用继承得硬造一条不自然的父类链;用协议 + 默认实现,可以把能力像插件一样组合(struct Bird: Flyable, Singable)。而且值类型也能用协议,不像继承只服务 class。
泛型:写出类型安全又通用的代码
泛型让函数/类型"延迟确定具体类型",既通用又不被丢进 Any 丧失类型检查。
现代并发:Structured Concurrency
Swift 5.5+ 引入 async/await,把回调地狱拍平成同步式写法,并引入"结构化并发"——子任务的生命周期绑定到父作用域。
论结构化并发解决什么老问题
旧时代用回调 + DispatchQueue,任务之间的关系是隐式的,取消要靠自己一层层传 flag,极易泄漏。结构化并发让"一群子任务"被当成一个整体:父任务取消,所有子任务自动取消;任一子任务抛错,能被统一捕获。代码可读、可取消、可推理。
Actor:用隔离保护可变状态
actor 是"自带串行锁的引用类型(reference type)":它内部的属性只能由它自己顺序访问,外部调用它的方法要 await,从语言层面消灭数据竞争。(注意:actor 与 class 一样是引用类型,而非 struct/enum 那种值类型。)
Actor 防止"同时改",但不防止"逻辑上依次改但结果不符合预期"。而且跨 actor 调用都要 await,把大对象塞进 actor 当全局锁用会成性能瓶颈。它替代的是手动锁,不是让你把一切状态都丢进一个巨型 actor。
错误处理:throws / Result / 断言
Swift 用 throw/throws/try 做显式错误传播,也可用 Result 把成功/失败装进一个值,适合回调场景。
SwiftUI 声明式 UI:界面就是状态的函数
SwiftUI 是苹果 2019 年推出的声明式 UI 框架:你描述"界面在某种状态下长什么样",状态一变框架自动重绘。这一章讲清 View 协议、状态属性、布局与导航。
View 协议与 body:最小页面长这样
所有 SwiftUI 视图都遵守 View 协议,只需实现一个 body 计算属性,返回"当前状态下该长什么样"。
论some View 是什么
SwiftUI 的 body 必须返回"某个具体的 View 类型",但不想写死(因为嵌套后类型很复杂)。some View 是不透明返回类型:对外只承诺"返回一种确定的 View",编译器在内部推断真实类型。既保住静态类型检查,又不用你手写那个巨长的泛型类型。
状态属性的四种姿势(及新版 @Observable)
状态"该放哪一层"决定数据流向。下表是核心选择;Swift 5.9 又推出 @Observable 宏,比旧版 ObservableObject 更简洁。
| 属性包装器 | 作用域 | 用在哪里 |
|---|---|---|
@State | 当前视图私有 | 按钮计数这类临时状态 |
@Binding | 子视图改父视图 | 开关、输入框双向绑定 |
@ObservableObject | 外部可观察对象 | 跨视图共享的 ViewModel |
@EnvironmentObject | 全局注入 | 用户设置、主题 |
把本该放 @ObservableObject 的状态塞进 @State,结果父视图一重建,子视图状态就重置。状态该谁拥有,就放在谁那一层。另一个常见错:在 body 里创建 @State 对象——body 每次重绘都会 new 一遍,状态永远不持久。
布局:VStack / HStack / ZStack / Grid
SwiftUI 布局是"容器 + 子视图"的声明。堆叠方向、间距、对齐都用初始化参数表达,比 Auto Layout 的约束直观得多。
列表与导航:NavigationStack 取代老 API
列表用 List + ForEach;导航用 NavigationStack + navigationDestination,按数据驱动跳转,比老 NavigationLink(destination:) 更灵活。
论为什么用数据驱动的导航
老写法把"目标视图"硬编码进 Link,深层跳转、动态路径、状态恢复都很难。新 navigationDestination(for:) 用"要展示的数据类型"决定去哪,导航栈是可被序列化的路径,做 deep link、撤销回退、单元测试都更顺。
修饰符链与自定义 View
SwiftUI 的 .font()、.padding() 是"修饰符"——每次调用都返回一个包了原视图的新视图,从内到外层层包裹。把常用组合抽成自定义 View 或 View 扩展,复用就清爽了。
动画与过渡:声明式地"动起来"
SwiftUI 的动画是"描述起止状态 + 告诉它要动画",框架自己补间,不用手写帧。
UIKit 与生命周期:SwiftUI 不够时的退路
SwiftUI 已覆盖绝大多数场景,但遇到第三方只提供 UIKit 控件、或极复杂的自定义手势/动画时,仍需 UIKit。苹果提供桥接机制让两者共存。这一章讲 UIViewController 生命周期、Auto Layout 约束、以及与 SwiftUI 的互通。
UIViewController 生命周期:视图什么时候建/卸
UIKit 是命令式的,控件要你手动创建、手动刷新。理解生命周期回调,才知道"初始化放哪、清理放哪"。
论viewDidLoad 和 viewWillAppear 的区别
viewDidLoad 整个生命周期只调一次,适合一次性的初始化(建子视图、注册通知)。viewWillAppear 每次视图要显示都调,适合随展示而变的东西(刷新数据、隐藏导航栏)。把"每次都要做"的事错放进 viewDidLoad,会导致来回切页面时不更新。
Auto Layout:用约束而非坐标摆界面
不同设备尺寸、横竖屏,不能写死 frame。Auto Layout 用"约束"(上下左右、宽高、比例)描述关系,系统求解出每个视图的位置。
① 忘了 translatesAutoresizingMaskIntoConstraints = false:系统会同时用新旧两套约束,冲突报错。② 约束不完整:缺一个维度(比如只有左右没高度)要么报错要么位置飘。用 UIStackView 或 NSLayoutConstraint 把约束写全。
SwiftUI 与 UIKit 混用(双向桥接)
两套可以互相嵌套:SwiftUI 里嵌 UIKit 用 UIViewRepresentable;UIKit 里嵌 SwiftUI 用 UIHostingController。迁移老项目时这是命脉。
论选型判断:90% SwiftUI,10% 桥 UIKit
新功能优先 SwiftUI;只有当官方 SwiftUI API 缺失、或要复用庞大旧代码时才下沉到 UIKit。不要为了"纯 SwiftUI"硬造轮子——能用 UIViewRepresentable 接现成 UIKit 控件是最务实的。
手势与 delegate 模式
UIKit 大量用 delegate(委托)模式解耦:一个对象把"该谁处理某事"委托给另一个。手势识别也是 UIKit 的强项。
Safe Area 与安全区域:别让内容钻进刘海
iPhone 有刘海/灵动岛和圆角,内容若铺满整个屏幕会钻进这些区域。UIKit 用 safeAreaLayoutGuide,SwiftUI 用 .safeAreaInset / 默认避开,确保内容落在"安全区域内"。
新手常把约束贴到 view.topAnchor(屏幕最顶)而非安全区,结果标题被刘海/状态栏盖住。统一用 safeAreaLayoutGuide(UIKit)或让 SwiftUI 自动处理,只有刻意要全屏背景时才 ignoresSafeArea。
App 架构:MVVM / Clean / VIPER 与导航
能跑的小 App 把逻辑塞进 ViewController 也行。但代码一多,"VC 变成几千行的上帝对象"是 iOS 项目的经典灾难。这一章讲主流架构如何分层、解耦、可测,以及导航与依赖注入怎么落地。
MVVM:把界面逻辑从 View 里请出去
Model–View–ViewModel 是最常用的起点:View 只管展示、ViewModel 持有状态与业务逻辑、Model 是数据。SwiftUI 的 @Observable 天然适配 VM。
论MVVM 解决什么
把"网络请求、校验、状态转换"从 View 里抽出来,ViewModel 不依赖任何 UI 框架,因此能脱离界面跑单元测试。View 只做"把状态画出来 + 把用户操作转成 VM 调用"。VC/View 从几千行瘦到几百行。
Clean 架构:再切一层用例
项目再大,连 VM 都开始臃肿。Clean 在 VM 之下加"用例(Use Case)"层,把"登录""拉列表"这类业务规则独立成可复用、可单测的小单元,VM 只编排它们。
VIPER:最重但最解耦,适合大团队协作
VIPER 把职责切到极致:View / Interactor(业务)/ Presenter(展示逻辑)/ Entity(模型)/ Router(导航)。样板多,但每个文件极小、可测、并行开发不冲突。
| 角色 | 负责 |
|---|---|
| View | 展示、接收用户操作,不含逻辑。 |
| Interactor | 核心业务规则与数据获取。 |
| Presenter | 把 Interactor 的结果转成 View 能直接画的东西。 |
| Entity | 纯数据模型。 |
| Router | 页面跳转与依赖装配。 |
Coordinator 导航模式:把"跳哪"从 VC 里拿走
默认做法里 VC 互相 push 下一个 VC,导致 VC 之间紧耦合、难以复用和测试。Coordinator 专门管"导航流",VC 只发"用户想前进"的事件。
论为什么导航要独立
VC 紧耦合跳转会让"复用这个页面到别处""写无界面的导航测试"都变难。Coordinator 把流程当成可替换的配置:同一套页面,登录后去主页还是去引导页,只改 Coordinator 一处。
依赖注入:别在内部 new 依赖
把依赖(网络层、数据库)从外部传进来,而不是在类里直接 RealService()。好处:测试时传假实现(Mock),生产换真实现,零改业务代码。
到处 AuthService.shared 看起来方便,但测试时没法替换、容易形成隐式耦合网。用构造注入(或工厂),依赖关系显式可见,重构和测试都轻松。
架构选型对比:没有银弹
| 架构 | 解耦程度 | 何时选 | 代价 |
|---|---|---|---|
| 无架构 | 低(VC 上帝对象) | Demo / 一次性 | 后期难维护 |
| MVVM | 中 | 大多数 App | VM 可能变胖 |
| Clean | 高 | 中大型、业务复杂 | 目录层级多 |
| VIPER | 极高 | 大团队、长生命周期 | 样板极多 |
持久化:从 UserDefaults 到 SQLite
App 关掉再打开,数据还在——这就是持久化。iOS 方案按"量级与复杂度"分档:小配置、敏感凭证、结构化数据库、轻量关系库。这一章把选型与写法讲清。
UserDefaults 与 Keychain:小配置 vs 敏感凭证
UserDefaults 存轻量偏好(开关、上次登录名),明文、不安全。密码、token 必须进 Keychain——它是系统级加密保险箱,卸载 App 也不丢。
UserDefaults 本质是明文 plist,越狱/备份可被读取。任何凭证(密码、token、密钥)只能放 Keychain,且必要时用 kSecAttrAccessibleWhenUnlocked 控制解锁后才可读。
Core Data / SwiftData:苹果官方结构化存储
SwiftData(iOS 17+)是 Core Data 的现代封装,用 @Model 宏 + 原生 Swift 类型,几乎零样板。适合"对象关系 + 可查询"的本地数据。
论SwiftData 还是 Core Data?
新项目、iOS 17+ 直接用 SwiftData,API 现代、和 Swift 类型系统贴合。要支持 iOS 16 及以下,或要用 Core Data 的高级特性(复杂迁移、批量处理),才退回 Core Data。两者底层是同一套存储引擎。
Realm:跨平台、易上手的对象数据库
Realm 是第三方对象数据库,API 比 Core Data 友好,且 Android/iOS 通用——跨端团队重用数据层时有优势。但引入了额外依赖与迁移机制。
SQLite 与 SQLite.swift:需要裸 SQL 时
当你要复杂联表、聚合、或与其他系统共用 .sqlite 文件时,直接上 SQLite。SQLite.swift 提供类型安全的 Swift 封装,比 C API 好用太多。
方案对比:按量级与复杂度选
| 方案 | 适合 | 代价 |
|---|---|---|
UserDefaults | 小配置、开关、非敏感 | 明文、不宜存大量 |
Keychain | 密码、token、密钥 | API 偏底层 |
SwiftData | 结构化、可查询(iOS17+) | 系统版本要求 |
Realm | 跨端对象库、易上手 | 额外依赖 |
SQLite.swift | 复杂 SQL、联表聚合 | 要写表/列定义 |
打包与性能:证书、TestFlight 与 Instruments
iOS 上架比安卓严格:隐私权限必须声明用途,审核会真的打开 App 逐项看。而性能上,卡顿和内存泄漏靠 Instruments 实证。这一章讲清证书体系、TestFlight、以及用 Leaks / Time Profiler 抓问题。
证书与签名体系:四件套认身份
iOS 用"证书 + 描述文件 + Bundle ID + 设备 UDID"四件套,确保只有被授权的代码能在被授权的设备上跑。先理解这张对照表,配签名才不懵。
| 组件 | 干什么 |
|---|---|
| Bundle ID | App 唯一身份证,和开发者账号绑定。 |
| 证书 Certificate | 证明"这个包确实是你签发的"(开发/分发两种)。 |
| 描述文件 Profile | 把证书 + Bundle ID + 设备绑在一起,真机/上架都用它。 |
| 设备 UDID | 开发/Ad Hoc 时限定能装的实体设备。 |
论为什么 iOS 签名这么烦
苹果用这套机制保证只有被你授权的代码能在被你授权的设备上跑,这是它生态封闭但 malware 极少的核心。日常用 Xcode 的"自动签名(Automatically manage signing)"能省掉八成手动配置;只有企业分发/CI 才需要手动管证书。
TestFlight:上架前先测后上
TestFlight 让你把构建版发给内部(最多 100 人,免审核)或外部测试员(需轻量审核),绕开完整 App Review 快速验证。
外部测试仍要苹果轻量审核,且构建版有 90 天有效期,过期测试员装不了。别把它当正式分发渠道——它只是"上架前快速验证"的通道。
Instruments 概览:iOS 的"听诊器"
Instruments 是 Xcode 自带的性能套件,能采样 CPU、内存、电量、网络。卡顿和泄漏都靠它定位,而不是靠"我感觉有点卡"。
| 模板 | 抓什么 |
|---|---|
| Time Profiler | CPU 占用与调用栈,定位耗时函数。 |
| Leaks | 内存泄漏(该释放却没释放的对象)。 |
| Allocations | 对象分配总量,看内存增长曲线。 |
| Core Animation | 离屏渲染、帧率、掉帧。 |
Leaks:抓住内存泄漏
iOS 用 ARC 自动管理引用计数,但循环引用(retain cycle)会让对象永远释放不掉。典型是闭包捕获 self 没用 [weak self],或 delegate 用了强引用。
论为什么 ARC 还会泄漏
ARC 只在"引用计数归零"时释放。一旦 A 引用 B、B 又引用 A(或对象持有强引用自己的闭包),计数永远不为 0,就泄漏。用 Leaks 模板跑一遍,红色小旗就是泄漏点,点进去能看到是哪条引用环。
Time Profiler:定位卡顿的耗时函数
卡顿(掉帧)通常是某个函数主线程干了太久的活。Time Profiler 采样调用栈,火焰图里最宽的柱子就是热点。
任何超过 ~16ms 的同步工作都会掉帧(60fps)。网络、大文件 IO、重解码、复杂计算全要离开主线程(用 Task/Task.detached 或 DispatchQueue)。别在主线程做这些,哪怕"就一次"。
离屏渲染与卡顿优化清单
某些视觉效果(圆角+裁剪、阴影、模糊)会触发"离屏渲染"——先把内容渲染到离屏缓冲再合成,开销大。下面是常用优化清单。
| 优化项 | 做法 |
|---|---|
| 避免不必要的圆角+mask | 用预处理图或 cornerRadius 而非 layer 遮罩 |
| 列表 cell 复用 | UITableView 自动复用,SwiftUI List 已优化 |
| 图片降采样 | 按显示尺寸解码,别拿 4000px 图塞 100px 视图 |
| 减少视图层级 | 扁平化层级,少嵌套 Container |
iOS 开发强约束、高价值:Swift 用可选值把"空"管进类型、用值类型(struct/enum)与 Actor 换取并发安全,面向协议(POP)比继承更灵活可组合,async/await + 结构化并发把回调拍平。SwiftUI 以"状态→界面"的声明式范式降低复杂度,状态属性按作用域分层(@State/@Binding/@Observable/@EnvironmentObject),布局用栈式容器、导航用数据驱动的 NavigationStack;UIKit 作为必要退路通过 UIViewRepresentable / UIHostingController 共存,且要懂 VC 生命周期与 Auto Layout 约束。App 架构按规模选 MVVM / Clean / VIPER,导航交给 Coordinator、依赖用构造注入而非全局单例。持久化按量级分档:UserDefaults 存配置、Keychain 存凭证、SwiftData/Core Data 存结构化数据、Realm 跨端、SQLite.swift 做复杂 SQL。上架需证书/描述文件/隐私清单且审核严格;TestFlight 是先测后上标准动作;性能靠 Instruments 的 Time Profiler(CPU 热点)、Leaks(循环引用泄漏)、Allocations 实证,主线程严禁重活,离屏渲染要规避。
1.Swift 里 struct 和 class 最关键的运行时区别是什么?为什么推荐默认用 struct?
查看答案
struct 是值类型(拷贝语义),class 是引用类型(共享语义)。值类型无隐式共享,多线程下更安全、数据流向更清晰,所以苹果推荐默认 struct,需要共享/继承才用 class。
2.@State 和 @ObservableObject 分别该用在什么场景?状态放错层会怎样?
查看答案
@State 管当前视图私有的临时状态(如计数器);@ObservableObject/@Observable 管跨视图共享、来自外部的 ViewModel 状态。放错层:把共享状态塞进 @State,父视图一重建子视图状态就重置;在 body 里 new @State 对象则永远不持久。
3.ARC 自动管理内存,为什么还会发生内存泄漏?怎么抓?
查看答案
循环引用(retain cycle)会让引用计数永远不为 0:典型是闭包强捕获 self、或 delegate 用强引用。解决:闭包用 [weak self]、delegate 用 weak。用 Instruments 的 Leaks 模板跑一遍,红色小旗即泄漏点,可看引用环。
4.密码类凭证为什么不能放 UserDefaults,该放哪?
查看答案
UserDefaults 本质是明文 plist,越狱/备份可读取。密码、token、密钥必须放 Keychain——系统级加密保险箱,按 kSecAttrAccessible 控制可读时机,卸载 App 也不丢。
5.主线程做了一次"就一次"的大 JSON 解析,为什么仍会卡顿?正确做法?
查看答案
主线程任何超过约 16ms 的同步工作都会掉帧(60fps),用户直接感知卡顿。正确做法:把网络/大文件 IO/重解码/重计算丢到后台(Task.detached / DispatchQueue),主线程只等结果。
下一步往哪走
路学完 iOS 之后
① 对照 Flutter 页:理解两套移动范式如何共用"平台通道 / 原生能力"的思路,做跨端架构决策。
② 做真机 App:从"网络列表 + 本地缓存 + 上架"的最小闭环做起,把证书、隐私清单、审核流程走通一次。
③ 进阶:动画与手势深入、Combine / 异步流、Widget 与推送、自动化发布(fastlane)、以及把 Instruments 当成日常排查习惯。