SwiftUI 从零到一:面向前端/RN 开发者的 iOS 指南
适用人群:有前端开发(React/Vue/Svelte)或 React Native 经验,但从未接触过 iOS 开发。 技术栈基准:Swift 6.2 + SwiftUI (iOS 26) + Xcode 26 目标:读完能独立开发并上架一个完整的 iOS App。 预计阅读时间:5-6 小时(建议分 6-8 次读完,配合动手实践)。
目录
- 第 0 章:iOS 开发全景图
- 第 1 章:Xcode 26 快速上手
- 第 2 章:Swift 速览 —— 给前端开发者的映射手册
- 第 3 章:声明式 UI —— 从 React 到 SwiftUI 的思维转变
- 第 4 章:第一个 SwiftUI App —— 逐行详解
- 第 5 章:View 的构成、修饰符与组合模式
- 第 6 章:状态管理深度解析
- 第 7 章:布局系统
- 第 8 章:列表、集合与数据展示
- 第 9 章:导航架构
- 第 10 章:动画系统
- 第 11 章:Swift 6 并发编程
- 第 12 章:网络请求与数据层
- 第 13 章:数据持久化
- 第 14 章:性能优化
- 第 15 章:无障碍与国际化
- 第 16 章:从开发到上架
- 附录 A:React → SwiftUI 速查表
- 附录 B:常见错误与调试
- 附录 C:术语表
第 0 章:iOS 开发全景图
0.1 你需要知道的几个”角色”
在开始写代码之前,先理解 iOS 开发生态中几个关键角色:
| 角色 | 是什么 | 前端类比 |
|---|---|---|
| Xcode | Apple 官方 IDE | VS Code + Chrome DevTools + webpack 打包,但更重、更集成 |
| Swift | 编程语言 | TypeScript——现代、类型安全,但更强(值类型、协议、宏) |
| SwiftUI | UI 框架 | React——声明式 UI,组件化,单向数据流 |
| UIKit | 旧版 UI 框架 | jQuery 时代的 DOM 操作——命令式,仍在大量使用 |
| iOS SDK | 系统能力集合 | 浏览器 API(相机、定位、推送、蓝牙等) |
| Simulator | iOS 模拟器 | Chrome DevTools 的设备模拟器 |
| App Store Connect | 上架管理后台 | Vercel / App Store 后台 |
| TestFlight | 内测分发 | Vercel Preview 部署 / 蒲公英 |
| SPM | 包管理器 | npm / yarn |
0.2 为什么是 SwiftUI 而不是 UIKit?
2026 年的今天,这个问题已经很简单了:
- SwiftUI 是 Apple 的未来方向。iOS 26 的 Liquid Glass 设计语言原生支持 SwiftUI,UIKit 几乎不再新增 API。
- 新 App 默认用 SwiftUI。Xcode 26 新建项目时,SwiftUI 是默认选项。
- UIKit 仍然需要了解。某些高级场景(自定义相机、复杂手势、地图等)可能还需要 UIKit,但作为新手,你可以先把 SwiftUI 学好,需要时再学 UIKit。
0.3 你需要一台 Mac
这是 iOS 开发唯一的硬件门槛。Xcode 只能在 macOS 上运行。最低配置建议:
- Mac:Apple Silicon (M1+) 芯片,16GB+ 内存
- macOS:最新版本(与 Xcode 26 兼容)
- iPhone(可选但强烈推荐):用于真机测试
0.4 开发流程概览
写代码 → Xcode Preview 实时预览 → 模拟器运行 → 真机测试 → 打包 → TestFlight 内测 → 提交审核 → 上架
SwiftUI 有一个很大的优势:Xcode 的 Preview 画布可以实时预览 UI,不需要每次都编译运行整个 App。这比 React Fast Refresh 还快,因为它是编译时的,不需要运行时热替换。
0.5 前端 → iOS 概念速查
这张表是你整个学习过程中的”翻译词典”,建议收藏:
| 前端/RN 概念 | iOS/SwiftUI 对应 | 说明 |
|---|---|---|
| VS Code | Xcode | 更重、更集成,但有 AI 助手 |
| npm/yarn | Swift Package Manager (SPM) | 包管理 |
| React | SwiftUI | 声明式 UI 框架 |
| JSX | @ViewBuilder 闭包 | 描述 UI 结构 |
useState |
@State |
视图私有状态 |
useContext |
@Environment |
全局共享状态 |
| props | let 参数 / @Binding |
数据传递 |
| children | @ViewBuilder 闭包参数 |
内容组合 |
useEffect |
.task / .onChange |
副作用处理 |
React.memo |
提取独立 View struct | 性能优化 |
| StyleSheet | 修饰符链 | 样式定义 |
| Flexbox | HStack / VStack / Grid | 布局系统 |
| React Navigation | NavigationStack / TabView | 导航 |
| AsyncStorage | SwiftData / @AppStorage | 持久化 |
| fetch / axios | URLSession | 网络请求 |
| Context Provider | .environment() |
依赖注入 |
| npm run build | Xcode Archive | 构建打包 |
第 1 章:Xcode 26 快速上手
Xcode 是你写 iOS App 的唯一工具。对前端开发者来说,它相当于 VS Code + Chrome DevTools + webpack + npm 的合体。这一章帮你快速熟悉它。
1.1 Xcode 界面布局
打开 Xcode 后,你会看到几个关键区域:
┌─────────────────────────────────────────────────────┐
│ Toolbar (运行/停止/设备选择/AI助手) │
├──────────┬──────────────────────┬───────────────────┤
│ │ │ │
│ Navigator│ Editor │ Inspector │
│ (文件树) │ (代码编辑器) │ (属性面板) │
│ │ │ │
│ ├──────────────────────┤ │
│ │ Debug Area │ │
│ │ (调试控制台) │ │
├──────────┴──────────────────────┴───────────────────┤
│ Canvas (Preview 画布) │
└─────────────────────────────────────────────────────┘
前端类比: - Navigator ≈ VS Code 侧边栏的文件树 - Editor ≈ VS Code 的代码编辑区 - Inspector ≈ Chrome DevTools 的 Elements 面板(查看/修改属性) - Canvas ≈ 浏览器的实时预览(但更快,编译时级别) - Debug Area ≈ Chrome DevTools 的 Console
1.2 创建你的第一个项目
- 打开 Xcode → File → New → Project
- 选择 iOS → App
- 填写:
- Product Name:
MyFirstApp - Interface: SwiftUI
- Language: Swift
- Product Name:
- 点击 Next,选择保存位置
类比:≈
npx create-react-app my-first-app,但通过 GUI 操作。
项目创建后,你会看到两个关键文件: - MyFirstAppApp.swift
—— App 入口(≈ index.js / App.tsx) -
ContentView.swift —— 根视图(≈ App.tsx 的
return 部分)
1.3 Preview 画布 —— SwiftUI 的杀手级功能
点击编辑器右上角的 “Canvas” 按钮(或按 ⌥⌘⏎),会打开
Preview 画布。它能:
- 实时预览 UI:修改代码后,画布自动更新(比 React Fast Refresh 更快)
- 交互式预览:点击画布中的按钮、输入文字、滚动,和真实 App 一样
- 多设备预览:同时预览 iPhone、iPad、不同尺寸
- 深色模式预览:右键画布 → Color Scheme → Dark
这是 SwiftUI 相比 React Native 最大的优势之一——你不需要编译运行整个 App 就能看到 UI 变化。
1.4 Xcode 26 AI 编程助手
Xcode 26 集成了 AI 编程助手,支持多种模型:
配置方式: - Xcode → Settings → Intelligence - 支持 ChatGPT(内置,有免费额度)、Claude(需要 API Key)、本地模型(Ollama/LM Studio)
核心能力: -
代码生成:描述你想要的功能,AI 直接生成代码 -
代码解释:选中代码 → 右键 → Explain(类似 GitHub
Copilot Chat) - 修复错误:编译错误旁会出现 “Generate
Fix” 按钮 - 修改历史:AI 每次修改前会创建快照,可以回溯
- #Playground 宏:在任何文件中插入内联
Playground,实时查看代码结果
前端类比:≈ GitHub Copilot + VS Code 的 AI 扩展,但更深度集成。
1.5 模拟器操作
运行 App(按 ⌘R)会启动 iOS 模拟器。常用操作:
| 操作 | 方法 |
|---|---|
| 旋转屏幕 | Hardware → Rotate Left/Right,或 ⌘← /
⌘→ |
| 模拟推送 | 拖拽 .apns 文件到模拟器 |
| 深色模式 | Features → Toggle Appearance,或 ⇧⌘A |
| 截图 | File → New Screen Shot,或 ⌘S |
| 模拟定位 | Features → Location → 自定义位置 |
前端类比:≈ Chrome DevTools 的设备模拟器,但更接近真实设备。
1.6 调试基础
断点调试
点击代码行号左侧可以设置断点。运行时程序会在断点处暂停,你可以: - 查看变量值(类似 Chrome DevTools 的 Scope) - 单步执行(Step Over / Step Into) - 在控制台执行 LLDB 命令(类似 Chrome Console)
View Hierarchy Debugger
调试栏中间的 3D 图标按钮。它会把你的视图层级拆成 3D 展示,类似 Chrome DevTools 的 Elements 面板,但更直观。
SwiftUI Performance Instrument(iOS 26 新增)
Xcode 26 新增的 SwiftUI 专用性能分析工具,可以查看: - 每个 View 的 body 执行时间 - 状态变化的因果图(哪个状态变化触发了哪个视图更新) - 主线程工作时间线
前端类比:≈ React DevTools 的 Profiler,但更底层、更精确。
1.7 Swift Package Manager (SPM)
SPM 是 Swift 的包管理器,≈ npm/yarn。
添加依赖: 1. File → Add Package Dependencies 2.
输入包的 GitHub URL(如
https://github.com/onevcat/Kingfisher) 3. 选择版本规则(Up
to Next Major 约等于 ^) 4. 点击 Add Package
常用包推荐:
| 包 | 用途 | 前端类比 |
|---|---|---|
| Kingfisher | 图片加载和缓存 | react-native-fast-image |
| Alamofire | 网络请求封装 | axios |
| SwiftLint | 代码风格检查 | ESLint |
| KeychainAccess | Keychain 封装 | 没有对应物(浏览器没有这个概念) |
第 2 章:Swift 速览 —— 给前端开发者的映射手册
我不会从零开始教你 Swift 语法,而是用对比帮你快速建立映射。如果你写过 TypeScript,你会在 30 分钟内看懂 Swift。但有几个概念是前端开发者必须深入理解的,我会用 ⚠️ 标注。
2.1 变量与常量
// Swift 有 var 和 let 两种声明
var name = "Alice" // 可变变量(≈ JS 的 let)
name = "Bob" // 可以修改
let age = 25 // 不可变常量(≈ JS 的 const,但更严格)
// age = 26 // 编译错误!关键区别:Swift 的 let 比 JS 的
const 更常用。Swift 社区强烈推荐”能用 let 就用
let“,编译器也会在你不修改某个变量时提示你改为
let。
⚠️ 前端陷阱:Swift
是强静态类型。var x = 0 后,x 被推断为
Int,你不能把 String 赋给它。TypeScript
开发者应该很熟悉这个约束。
2.2 类型系统
// 基本类型
let integer: Int = 42
let double: Double = 3.14
let text: String = "Hello"
let flag: Bool = true
// 类型注解(可选,编译器能推断)
var count: Int = 0
var prices: [Double] = [9.99, 19.99]⚠️ 前端陷阱:Swift 没有 number
类型,区分 Int(整数)和
Double(浮点数)。let x = 42 是
Int,let x = 42.0 是
Double。两者不能直接混用运算。
2.3 Optional —— 前端开发者最需要理解的概念
这是 Swift 最核心的特性之一。它强制你在编译时处理”值可能不存在”的情况。
// ? 表示这个值可能是 nil(≈ TypeScript 的 string | null)
var maybeName: String? = "Alice"
maybeName = nil // 可以是 nil
// 安全解包(≈ JS 的 if (name != null))
if let name = maybeName {
print("Hello, \(name)") // 只有 name 有值时才执行
}
// 守卫解包(提前退出)
guard let name = maybeName else {
print("No name!")
return
}
print("Hello, \(name)") // name 在这里一定有效
// 空合运算符(≈ JS 的 ?? 或 ||)
let displayName = maybeName ?? "Anonymous"⚠️ 前端陷阱 1:Swift 没有
undefined,只有 nil。nil
不是值,是”没有值”。你不能把 nil 赋给非 Optional
类型的变量。
⚠️ 前端陷阱 2:不能直接用 Optional
值。maybeName.count 会编译错误,必须先解包。这和 JS/TS
不同——JS 允许你对 null 访问属性(运行时报错),Swift
在编译时就阻止了。
⚠️ 前端陷阱 3:Optional 链(?.)在
Swift 中也存在:
let count = maybeName?.count // count 是 Int?(还是 Optional)2.4 结构体与类 —— 最大的思维差异
这是前端开发者最容易困惑的地方。JS/TS 的对象都是引用类型,但 Swift 有两种类型:
Struct(值类型)—— 拷贝传递:
struct User {
var name: String
}
var a = User(name: "Alice")
var b = a // 拷贝!不是引用!
b.name = "Bob"
print(a.name) // "Alice" —— a 不受影响Class(引用类型)—— 共享引用:
final class Counter {
var count: Int = 0
}
let a = Counter()
let b = a // 指向同一个对象!
b.count = 42
print(a.count) // 42 —— 两者是同一个实例⚠️ 前端陷阱 1:Swift 的 struct
是拷贝,不是引用。这和 JS
的对象完全不同。var b = a 在 JS 中是引用,在 Swift 的
struct 中是深拷贝。
⚠️ 前端陷阱 2:SwiftUI 的 View 都是
struct。这意味着每次视图更新时,View struct 会被销毁重建。但
@State 的值不会丢失,因为它存储在 SwiftUI
框架管理的独立内存中。
⚠️ 前端陷阱 3:修改 struct 的属性,变量必须用
var 声明。let 声明的 struct
不能修改任何属性,即使属性本身是 var。
选择原则: - 默认用 struct(Swift 社区共识) - 只有需要共享可变状态时才用 class(配合 @Observable) - SwiftUI 中:View 用 struct,状态管理用 class
2.5 枚举与关联值
Swift 的枚举比 TypeScript 强大得多——可以携带关联值:
// 简单枚举(≈ TS 的 enum)
enum Direction {
case north, south, east, west
}
// 带关联值的枚举(≈ TS 的 discriminated union)
enum Route {
case home
case profile(userID: String) // 携带数据
case settings
case detail(id: Int, title: String) // 携带多个值
}
// 使用
let route = Route.profile(userID: "123")
// 模式匹配(≈ TS 的 switch + 类型收窄)
switch route {
case .home:
print("Home")
case .profile(let userID): // 解构关联值
print("Profile: \(userID)")
case .settings:
print("Settings")
case .detail(let id, let title):
print("Detail \(id): \(title)")
}这个特性在路由、状态机、API 响应中极其常用。TypeScript 的 discriminated union 可以模拟类似效果,但 Swift 的枚举是原生的、更简洁的。
2.6 函数与闭包
// 函数定义
func greet(name: String, age: Int) -> String {
return "Hello, \(name)! You are \(age) years old."
}
// 调用时参数标签必须写(≈ Kotlin 的命名参数)
greet(name: "Alice", age: 25) // 不是 greet("Alice", 25)
// 省略参数标签
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
add(3, 5) // 可以用 _ 隐藏参数标签
// 闭包(≈ 箭头函数)
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { number in
return number * 2
}
// 简写形式(≈ numbers.map(n => n * 2))
let doubled2 = numbers.map { $0 * 2 } // $0 是第一个参数
// 尾随闭包 —— Swift 的特色语法
// 如果最后一个参数是闭包,可以写在括号外面
numbers.sorted { a, b in
return a > b
}⚠️ 前端陷阱:Swift
的闭包默认捕获引用(不是值)。如果闭包捕获了 class
实例,可能导致循环引用。用 [weak self] 避免:
someAsyncTask { [weak self] result in
self?.handleResult(result)
}2.7 协议与扩展
// 协议(≈ TypeScript Interface,但更强大)
protocol Identifiable {
var id: String { get }
}
// 遵循协议
struct Product: Identifiable {
let id: String
let name: String
}
// 扩展(给已有类型添加功能,JS 没有对应物)
extension String {
var isEmail: Bool {
return self.contains("@") && self.contains(".")
}
}
"test@example.com".isEmail // true
// 协议扩展(≈ Rust trait 默认实现)
extension Identifiable where Self: View {
var identifier: String { "view-\(id)" }
}2.8 属性包装器(Property Wrapper)
属性包装器是 SwiftUI 中最常用的语法,以 @ 开头:
// @State ≈ useState()
@State private var count = 0
// @Binding ≈ props 传递(子组件可以修改父组件的状态)
@Binding var isOn: Bool
// @Environment ≈ useContext()
@Environment(AppState.self) private var appState先记住用法,第 6 章会详细解释。
2.9 错误处理
// 定义可能抛出错误的函数
func loadData() throws -> Data {
guard let url = URL(string: "https://api.example.com") else {
throw URLError(.badURL)
}
return try Data(contentsOf: url)
}
// 调用时必须处理错误
do {
let data = try loadData()
// 使用 data
} catch {
print("Error: \(error)")
}
// try? 把错误转为 nil(≈ catch { return null })
let data = try? loadData() // data 是 Data?
// try! 强制解包(危险,崩溃时用)
let data = try! loadData() // 如果抛出错误,程序崩溃⚠️ 前端陷阱:Swift 的 throws
不是异步的。它和 JS 的 Promise.reject
不同——throws 是同步错误,async throws
才是异步错误。
2.10 guard 语句
// Swift 风格:提前退出
func processUser(_ user: User?) {
guard let user = user else {
print("No user")
return
}
// 这里 user 一定有效,且作用域在整个函数内
print("Hello, \(user.name)")
}
// 前端风格(不推荐在 Swift 中使用)
func processUser(_ user: User?) {
if let user = user {
// 这里 user 有效,但嵌套更深
print("Hello, \(user.name)")
} else {
print("No user")
}
}⚠️ 前端陷阱:Swift 开发者习惯用 guard
提前处理错误情况,让正常流程保持在最外层。这和前端的
if-else 嵌套风格不同。
2.11 小结:至少掌握这 5 个概念
letvsvar— 常量 vs 变量- Optional(
?和if let) — 处理可能为空的值 - Struct vs Class — 值类型 vs 引用类型
- 闭包(Closure) — 匿名函数,尾随闭包语法
- 属性包装器(
@State,@Binding等) — 先记住用法,后面会详细解释
第 3 章:声明式 UI —— 从 React 到 SwiftUI 的思维转变
如果你之前写过 React、Vue 或 Flutter,你已经理解了声明式 UI 的核心思想。这一章帮你建立 SwiftUI 和 React 之间的精确映射。
3.1 命令式 vs 声明式
命令式(UIKit / jQuery 时代):
// 这是 UIKit 的写法(不要学,只是对比用)
let label = UILabel()
label.text = "Hello"
label.textColor = .black
label.font = UIFont.systemFont(ofSize: 17)
view.addSubview(label)
// 需要更新时,手动修改
label.text = newText
label.textColor = newColor
// 你告诉系统"怎么做"——每一步都要手动操作声明式(SwiftUI ≈ React):
struct GreetingView: View {
var name: String
var isHighlighted: Bool
var body: some View {
Text("Hello, \(name)")
.font(.system(size: 17))
.foregroundStyle(isHighlighted ? .blue : .black)
}
}
// 你告诉系统"我想要什么"——SwiftUI 会自动处理更新3.2 SwiftUI 的更新机制
1. 某个 @State / @Observable 属性发生变化
2. SwiftUI 标记依赖该属性的视图为"需要更新"
3. 在下一个渲染周期,SwiftUI 重新执行 body
4. SwiftUI 比较新旧视图树(diffing)
5. 只有真正变化的部分才会更新屏幕上的像素
类比:≈ React 的 reconciliation,但 SwiftUI 的 diffing 更高效——它基于 struct 的值比较,而不是虚拟 DOM 的逐节点比较。
关键点: - body
可能被频繁调用(每次状态变化都可能触发) - 因此 body
必须保持轻量和纯粹 - 不要在 body
中做复杂计算、网络请求或副作用
3.3 与 React 的关键差异
| 维度 | React | SwiftUI |
|---|---|---|
| 组件类型 | 函数 / Class | struct(值类型) |
| 状态 | useState(hook) |
@State(属性包装器) |
| 渲染 | 虚拟 DOM diff | struct diffing |
| 副作用 | useEffect |
.task / .onChange |
| 组合 | children props | @ViewBuilder 闭包 |
| 样式 | CSS-in-JS / className | 修饰符链 |
| 生命周期 | mount / unmount | onAppear / onDisappear |
| 全局状态 | Context + Provider | .environment() |
| 性能优化 | React.memo + useMemo |
提取独立 View struct |
3.4 @ViewBuilder —— SwiftUI 的”JSX”
SwiftUI 没有 children props,用 @ViewBuilder
闭包替代:
// React: <Container><Text>Hello</Text></Container>
// SwiftUI:
Container {
Text("Hello")
}
// 定义接受内容的组件
struct Container<Content: View>: View {
@ViewBuilder let content: Content // ← 这就是 children
var body: some View {
content.padding()
}
}⚠️ 前端陷阱:SwiftUI 的 @ViewBuilder
不是 JSX。它是编译时的语法糖,会把闭包内的多个 View 表达式转换为
TupleView。运行时没有额外开销。
3.5 一个具体的思维转变例子
假设你要做一个”关注/已关注”按钮:
React 思维:
const [isFollowing, setIsFollowing] = useState(false)
return (
<button onClick={() => setIsFollowing(!isFollowing)}>
{isFollowing ? "已关注" : "关注"}
</button>
)SwiftUI 思维(几乎一模一样):
struct FollowButton: View {
@State private var isFollowing = false
var body: some View {
Button(isFollowing ? "已关注" : "关注") {
isFollowing.toggle()
}
.buttonStyle(.borderedProminent)
.tint(isFollowing ? .gray : .blue)
}
}核心思想完全一致:UI = f(state)。你定义状态,描述 UI 如何由状态决定,修改状态时 UI 自动更新。
第 4 章:第一个 SwiftUI App —— 逐行详解
4.1 创建项目
- 打开 Xcode → File → New → Project
- 选择 iOS → App
- 填写:
- Product Name:
MyFirstApp - Interface: SwiftUI
- Language: Swift
- Product Name:
- 点击 Next,选择保存位置
4.2 项目结构逐行解读
Xcode 会生成以下关键文件:
// MyFirstAppApp.swift —— App 入口文件(≈ index.js / main.tsx)
import SwiftUI // 导入 SwiftUI 框架(≈ import React from 'react')
@main // 标记:这是程序的入口点(≈ ReactDOM.render())
struct MyFirstAppApp: App { // 遵循 App 协议
var body: some Scene { // body 返回一个场景
WindowGroup { // 标准窗口场景
ContentView() // 根视图(≈ <App />)
}
}
}// ContentView.swift —— 根视图(≈ App.tsx)
import SwiftUI
struct ContentView: View { // 遵循 View 协议
var body: some View { // 必须实现 body 计算属性
VStack { // 垂直布局容器(≈ flex-direction: column)
Image(systemName: "globe") // SF Symbol 图标
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!") // 文本标签(≈ <Text>Hello</Text>)
}
.padding() // 内边距
}
}
#Preview { // 预览宏 —— 在 Xcode 画布中实时预览
ContentView()
}逐行解释:
import SwiftUI:导入框架(≈import React)@main:程序入口(≈ReactDOM.render())struct ... App:App 协议定义了应用的入口。SwiftUI 的 App 是 struct,不是 classVStack:垂直堆叠容器(≈<div style={{display:'flex', flexDirection:'column'}}>)Image(systemName:):使用 SF Symbols——Apple 提供的 6000+ 免费图标(≈ Font Awesome).padding():修饰符,给 VStack 加内边距#Preview:Xcode 预览宏,不需要运行 App 就能看到 UI
4.3 修饰符(Modifier)的正确理解
这是新手最容易困惑的地方之一。
修饰符不是”修改原始视图”,而是”包装原始视图,返回一个新视图”。修饰符的顺序决定了最终的视觉效果。
// 示例 1:先 padding 再 background
Text("Hello")
.padding(20) // 先加 20pt 的内边距
.background(.blue) // 背景色覆盖 padding 区域
// 结果:蓝色背景很大,文字周围有 20pt 的蓝色空间
// 示例 2:先 background 再 padding
Text("Hello")
.background(.blue) // 背景色只覆盖文字区域
.padding(20) // 在蓝色背景外面再加 20pt 透明空间
// 结果:蓝色背景很小,紧贴文字,外面有 20pt 透明空间理解方法:从下往上读修饰符链。最底层的修饰符先应用,然后往上包装。
前端类比:≈ CSS 的层叠,但更明确。CSS 中
padding 和 background 的关系取决于
background-clip,SwiftUI 中顺序直接决定效果。
4.4 动手:修改第一个 App
把 ContentView 改成这样:
struct ContentView: View {
@State private var name = ""
var body: some View {
VStack(spacing: 20) {
// 头像
Image(systemName: "person.circle.fill")
.resizable() // 允许缩放
.frame(width: 80, height: 80)
.foregroundStyle(.blue)
// 问候语
if name.isEmpty {
Text("What's your name?")
.font(.title2)
.foregroundStyle(.secondary)
} else {
Text("Hello, \(name)!")
.font(.title)
.fontWeight(.bold)
}
// 输入框
TextField("Your name", text: $name)
.textFieldStyle(.roundedBorder)
.frame(width: 250)
.multilineTextAlignment(.center)
}
.padding()
}
}运行
App(⌘R),在输入框输入名字,观察问候语如何实时变化。
关键理解: -
@State private var name = "" — 定义状态(≈
const [name, setName] = useState("")) -
text: $name — $name 创建一个
Binding<String>,让 TextField 可以读写 name(≈
受控组件) - 声明式思维:你只定义了 name 和 UI
如何展示它,SwiftUI 处理了所有更新逻辑
第 5 章:View 的构成、修饰符与组合模式
这一章是你写 SwiftUI 代码的基础。理解 View 的本质、修饰符体系和组合模式,你就能写出任何 UI。
5.1 View 协议的本质
protocol View {
associatedtype Body: View // 关联类型:body 返回的必须是 View
@ViewBuilder var body: Self.Body { get } // 唯一要求
}每个 SwiftUI 视图都是一个 struct,它唯一需要做的就是实现
body 计算属性。body 描述了这个视图的 UI
结构。
关键理解:SwiftUI 的 View 不是屏幕上显示的那个东西。它是一个轻量级的描述(蓝图),SwiftUI 框架在幕后根据这个描述创建和管理实际的渲染对象。
前端类比:≈ React 的 ReactElement / JSX。你写的
<div>Hello</div> 不是真正的 DOM
节点,而是一个描述。React 在幕后根据这个描述创建和更新真正的 DOM。
这解释了为什么 View 可以是 struct(值类型)而且可以频繁创建和销毁——创建 struct 几乎零开销,但创建 UIKit 的 UIView 开销很大。
5.2 常用容器视图
// 1. VStack —— 垂直排列(≈ flex-direction: column)
VStack(alignment: .leading, spacing: 12) {
Text("Title").font(.headline)
Text("Subtitle").font(.subheadline)
Text("Body content goes here")
}
// 2. HStack —— 水平排列(≈ flex-direction: row)
HStack(alignment: .center, spacing: 8) {
Image(systemName: "star.fill")
Text("Favorites")
Spacer() // 弹性空间(≈ flex: 1)
Text("\(count)")
}
// 3. ZStack —— 层叠排列(≈ position: absolute)
ZStack {
Image("background") // 底层
VStack {
Text("Title") // 上层
}
}
// 注意:ZStack 中后写的视图在上面
// 4. Group —— 逻辑分组(不改变布局)
// 用于:绕过 10 个子视图限制、共享修饰符
Group {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.foregroundStyle(.blue) // 所有子视图都会变蓝
// 5. ScrollView + LazyVStack —— 可滚动内容
ScrollView {
LazyVStack(spacing: 16) {
ForEach(items) { item in
ItemRow(item: item)
}
}
.padding()
}
// LazyVStack: 只创建屏幕上可见的视图(≈ 虚拟列表)
// 6. Form —— 系统风格表单(≈ iOS 原生表单样式)
Form {
Section("Profile") {
TextField("Name", text: $name)
TextField("Email", text: $email)
}
Section("Preferences") {
Toggle("Notifications", isOn: $notifications)
}
}5.3 修饰符深度解析
修饰符是 SwiftUI 的核心语法。每个修饰符都返回一个新的 View。
// 1. 样式修饰符 —— 改变视觉效果
Text("Hello")
.font(.title) // 字体
.fontWeight(.bold) // 字重
.foregroundStyle(.blue) // 前景色
.background(.yellow) // 背景色
.opacity(0.8) // 不透明度
.shadow(radius: 5) // 阴影
// 2. 布局修饰符 —— 改变大小和位置
Text("Hello")
.frame(width: 200, height: 100) // 固定尺寸
.frame(maxWidth: .infinity) // 最大宽度(撑满)
.padding(16) // 内边距
.padding(.horizontal, 20) // 水平方向内边距
.offset(x: 10, y: -5) // 偏移(不影响布局)
// 3. 行为修饰符 —— 添加交互
Button("Tap Me") { }
.buttonStyle(.borderedProminent)
.disabled(true)
// 4. 条件修饰符
Text("Status")
.foregroundStyle(isError ? .red : .primary)
.font(isTitle ? .title : .body)⚠️ 前端陷阱
1:修饰符顺序很重要。.padding().background(.blue)
和 .background(.blue).padding()
效果完全不同。从下往上读修饰符链。
⚠️ 前端陷阱 2:SwiftUI 没有 CSS 的
display: none。隐藏视图用 .opacity(0)
或条件判断 if。.hidden()
会隐藏视图但仍然占位(≈ visibility: hidden)。
5.4 组合模式 —— React 开发者的必备知识
SwiftUI 的组件组合模式和 React 有显著差异。这里是完整的映射:
children 模式(@ViewBuilder 闭包)
// React: <Card><Text>Hello</Text></Card>
// SwiftUI:
Card {
Text("Hello")
}
struct Card<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
content
.padding()
.background(.regularMaterial)
.clipShape(.rect(cornerRadius: 12))
}
}Slot 模式(命名的闭包参数)
// React: <Page header={<Header />} footer={<Footer />} />
// SwiftUI:
PageLayout(
header: { HeaderView() },
footer: { FooterView() }
)
struct PageLayout<Header: View, Footer: View>: View {
@ViewBuilder let header: Header
@ViewBuilder let footer: Footer
var body: some View {
VStack {
header
Spacer()
footer
}
}
}Factory 模式(返回 View 的闭包)
// 类似 React 的 render props
struct LazyView<Content: View>: View {
let build: () -> Content
var body: some View {
build() // 只在 body 执行时才创建内容
}
}5.5 提取子视图的时机
用计算属性(var subview: some View):
- 视图只在一处使用 - 不需要独立的生命周期
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack {
headerView
counterView
}
}
private var headerView: some View {
Text("Dashboard")
.font(.largeTitle)
}
private var counterView: some View {
HStack {
Text("Count: \(count)")
Button("+1") { count += 1 }
}
}
}用独立 struct: - 视图在多个地方复用 - 需要独立的状态管理 - 为了性能优化(SwiftUI 可以跳过不必要 body 执行)
前端类比:≈ React.memo +
拆分组件。提取独立 struct 后,SwiftUI 只有在参数变化时才重新执行
body。
5.6 Liquid Glass 设计系统(iOS 26)
iOS 26 引入了 Liquid Glass——Apple 自 iOS 7 以来最大的视觉革新。它是一个”数字元材质”,能动态弯曲和塑形光线。
基础用法
// 基础玻璃效果(默认胶囊形状)
Button("To Top", systemImage: "chevron.up") {
scrollToTop()
}
.padding()
.glassEffect()
// 自定义形状
Label("Desert", systemImage: "sun.max.fill")
.padding()
.glassEffect(in: .rect(cornerRadius: 16))
// 着色玻璃
Label("Desert", systemImage: "sun.max.fill")
.padding()
.glassEffect(.regular.tint(.green))
// 交互效果(缩放、弹跳、微光)
Label("Desert", systemImage: "sun.max.fill")
.padding()
.glassEffect(.regular.interactive())形态变换(Morphing)
@Namespace var namespace
GlassEffectContainer {
VStack {
if isExpanded {
ForEach(badges) { badge in
BadgeLabel(badge: badge)
.glassEffect()
.glassEffectID(badge.id, in: namespace)
}
}
BadgeToggle()
.buttonStyle(.glass)
.glassEffectID("badgeToggle", in: namespace)
}
}glassEffectID(_:in:) 使用 @Namespace
实现玻璃元素之间的流畅形态变换。
新按钮样式
Button("Get Started") { }
.buttonStyle(.glassProminent)
Button("Learn More") { }
.buttonStyle(.glass)
// 新尺寸
Button("Extra Large") { }
.controlSize(.extraLarge)设计原则
⚠️ 重要规则: - Liquid Glass 只用于导航层(Tab 栏、导航栏、工具栏),不用于内容层 - 不要堆叠玻璃(glass on glass) - 稳态下避免内容与 Liquid Glass 交叉 - 小元素(导航栏、Tab 栏)会自动适应明暗模式;大元素(侧边栏、菜单)不会自动切换 - 使用 tinting 高亮主要元素和操作,但不要给所有元素着色
5.7 避免的性能陷阱
// ❌ 不要在 body 中创建对象
struct BadView: View {
var body: some View {
let formatter = DateFormatter() // 每次 body 执行都创建!
return Text(formatter.string(from: Date()))
}
}
// ✅ 使用静态属性
struct GoodView: View {
private static let formatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .long
return f
}()
var body: some View {
Text(Self.formatter.string(from: Date()))
}
}
// ❌ 不要在 body 中做复杂计算
struct BadFilterView: View {
let items: [Item]
var body: some View {
List(items.filter { $0.isEnabled }.sorted { $0.name < $1.name }) { item in
Text(item.name)
}
}
}
// ✅ 预计算
struct GoodFilterView: View {
let items: [Item]
private var filteredItems: [Item] {
items.filter { $0.isEnabled }.sorted { $0.name < $1.name }
}
var body: some View {
List(filteredItems) { item in
Text(item.name)
}
}
}第 6 章:状态管理深度解析
这是 SwiftUI 最重要的一章。状态管理决定了你的数据如何流动、UI 如何更新。
6.1 核心概念:数据流向(单向数据流)
SwiftUI 的数据流是单向的——和 React 完全一致:
状态(Source of Truth)
↓
View 读取状态展示 UI
↓
用户交互 → 修改状态
↓
状态变化 → View 重新渲染
↓
(循环)
6.2 @State —— 视图私有的简单状态
使用场景:一个布尔值、一个字符串、一个数字——这些简单值只被当前视图使用。
struct CounterView: View {
@State private var count = 0 // ≈ const [count, setCount] = useState(0)
var body: some View {
VStack(spacing: 20) {
Text("Count: \(count)")
.font(.largeTitle)
HStack(spacing: 20) {
Button("-") { count -= 1 }
Button("+") { count += 1 }
}
}
}
}@State
的工作原理: 1. @State 告诉
SwiftUI:“这个值归这个视图所有,它的变化应该触发视图更新” 2. 当
count 改变时,SwiftUI 标记 CounterView
需要重新渲染 3. @State 的值存储在 SwiftUI
框架管理的独立内存中(不在 View struct 内) 4. 这意味着 View struct
被销毁重建时,@State 的值不会丢失
关键规则: - @State 必须声明为
private - 不要用 @State
接收从父视图传入的值——@State 只使用初始值,忽略后续更新 -
@State 适合值类型(Int, String, Bool, struct 等)
// ❌ 错误:用 @State 接收外部值
struct ChildView: View {
@State var name: String // 父视图更新 name 时,这里不会更新!
var body: some View {
Text(name) // 永远显示初始值
}
}
// ✅ 正确:用 let 接收
struct ChildView: View {
let name: String // 父视图更新时,这里也会更新
var body: some View {
Text(name)
}
}6.3 @Binding —— 子视图修改父视图的状态
使用场景:一个 Toggle 组件需要修改父视图的布尔值;一个 TextField 需要修改父视图的字符串。
// 父视图 —— 拥有状态
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleRow(isOn: $isOn) // $ 符号:传递 binding
}
}
// 子视图 —— 通过 Binding 修改父视图的状态
struct ToggleRow: View {
@Binding var isOn: Bool // ≈ props + onChange 回调
var body: some View {
Toggle("Enable Feature", isOn: $isOn)
}
}$ 符号的含义: - $isOn
创建了一个 Binding<Bool> 类型的值 -
它像一个”读写引用”,指向源状态 - 子视图通过 Binding
读取和修改源状态
@Binding
的规则: - 只用于子视图需要修改值的情况 -
如果子视图只读取值,用 let 就够了 - @Binding
不应该有初始值(它从外部获取)
6.4 @Observable —— 现代引用类型状态管理(iOS 17+)
使用场景:复杂的数据模型,需要被多个视图共享和修改。
// 1. 定义数据模型
@Observable // ← 这个宏自动生成所有观察能力
@MainActor // ← 标记为在主线程上操作(UI 安全)
final class UserModel {
var name = ""
var email = ""
var isLoggedIn = false
var isLoading = false
}
// 2. 在视图中创建和持有
struct ContentView: View {
@State private var model = UserModel() // 用 @State,不是 @StateObject
var body: some View {
VStack {
if model.isLoading {
ProgressView()
} else {
Text("Hello, \(model.name)")
}
}
.task {
model.isLoading = true
// 加载数据...
model.isLoading = false
}
}
}
// 3. 传递给子视图
struct UserProfileView: View {
@Bindable var model: UserModel // 需要 $model.name 时用 @Bindable
var body: some View {
Form {
TextField("Name", text: $model.name)
}
}
}@Observable 的关键规则:
标记类为
@MainActor:UI 相关状态必须在主线程操作在 @Observable 类中使用属性包装器必须加
@ObservationIgnored:
@Observable
@MainActor
final class SettingsModel {
// ✅ 必须加 @ObservationIgnored
@ObservationIgnored @AppStorage("isDarkMode") var isDarkMode = false
// 普通属性不需要
var isLoading = false
}- View 拥有 @Observable 对象时,必须用
@State:
// ✅ 正确
@State private var model = UserModel()
// ❌ 错误
let model = UserModel() // View 重建时 model 也会重建,丢失状态6.5 @Environment —— 全局共享状态
使用场景:需要在很多层级的视图中访问的数据——用户登录状态、主题设置等。
// 步骤 1:定义全局状态
@Observable
@MainActor
final class AppState {
var isLoggedIn = false
var currentUser: User?
}
// 步骤 2:在 App 入口注入
@main
struct MyApp: App {
@State private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environment(appState) // ← 注入到环境
}
}
}
// 步骤 3:在任意深度的子视图中读取
struct SomeDeeplyNestedView: View {
@Environment(AppState.self) private var appState // ← 从环境读取
var body: some View {
if appState.isLoggedIn {
Text("Welcome, \(appState.currentUser?.name ?? "")")
} else {
Button("Log In") {
appState.isLoggedIn = true
}
}
}
}@Environment 与直接传参的选择:
- 如果只有 1-2 层传递,直接传参更清晰 - 如果 3
层以上,或者很多视图都需要,用 @Environment -
系统提供的环境值(如
@Environment(\.colorScheme)、@Environment(\.dismiss))非常常用
6.6 状态管理决策流程图
这个值属于当前视图吗?
│
├─ 是 → 它是简单的值类型吗?
│ │
│ ├─ 是(Int, String, Bool, struct)→ @State private var
│ │
│ └─ 否(class)→ 用 @Observable 标记 class
│ └─ 在视图中:@State private var model = MyModel()
│
└─ 否(从父视图传入)→
│
├─ 子视图需要修改它吗?
│ └─ 是 → @Binding var
│
├─ 子视图需要访问它的属性绑定(如 $model.name)吗?
│ └─ 是(是 @Observable class)→ @Bindable var
│
└─ 只是读取和展示 → let
6.7 状态管理性能
⚠️ 重要区别:
@State:只触发声明它的视图重绘。其他读取了同一个 @State 的视图不受影响。@Observable:触发所有读取了被修改属性的视图重绘。
这意味着如果你有一个 @Observable 的 model,修改
model.name 会导致所有读取了 model.name
的视图重绘——即使它们在完全不同的视图层级。
优化策略: - 传递最小依赖:不要把整个 model 传给子视图,只传需要的值 - 提取独立 struct:让 SwiftUI 可以跳过不必要 body 执行 - 使用计算属性派生状态:不要存储派生状态
6.8 旧 API 与新 API 对照
如果你在网上看到以下旧 API,记住它们的现代替代品:
| 旧 API(iOS 16 及以前) | 新 API(iOS 17+) | 说明 |
|---|---|---|
class MyModel: ObservableObject |
@Observable class MyModel |
样板代码减少 70% |
@Published var name = "" |
var name = "" |
不需要 @Published 了 |
@StateObject var model = MyModel() |
@State var model = MyModel() |
@State 统一管理 |
@ObservedObject var model: MyModel |
@Bindable var model: MyModel |
需要绑定时用 @Bindable |
第 7 章:布局系统
7.1 SwiftUI 的布局三步舞
SwiftUI 的布局过程分为三个阶段:
1. 父视图提出大小建议(Propose)
父视图告诉子视图:"我建议你这么大"
2. 子视图决定自己的大小(Determine)
子视图根据建议和自己的内容,返回一个大小
3. 父视图把子视图放到指定位置(Position)
父视图根据子视图的大小,把它放到合适的位置
⚠️ 前端陷阱:SwiftUI 的布局是”父驱动”,CSS 是”子驱动”。在 CSS 中,元素可以自己决定大小(除非约束),父元素被动适应。在 SwiftUI 中,是父视图主动建议,子视图响应建议。
7.2 Flexbox → SwiftUI 布局映射
这是前端开发者最需要的映射表:
| Flexbox | SwiftUI | 说明 |
|---|---|---|
display: flex |
HStack / VStack | 容器 |
flex-direction: row |
HStack | 水平排列 |
flex-direction: column |
VStack | 垂直排列 |
justify-content: space-between |
Spacer() | 弹性空间 |
align-items: center |
alignment: .center | 对齐 |
gap: 10 |
spacing: 10 | 间距 |
flex: 1 |
.frame(maxWidth: .infinity) | 撑满 |
flex-wrap: wrap |
LazyVGrid | 网格 |
position: absolute |
ZStack + offset | 层叠 |
overflow: scroll |
ScrollView | 滚动 |
width: 100px |
.frame(width: 100) | 固定宽度 |
padding: 10px |
.padding(10) | 内边距 |
margin: 10px |
没有直接对应 | 用 padding 或 Spacer |
7.3 常用布局修饰符
// frame —— 设置尺寸
Text("Hello")
.frame(width: 200, height: 100) // 固定尺寸
.frame(maxWidth: .infinity) // 最大宽度撑满
.frame(minWidth: 100, idealWidth: 200) // 最小/理想宽度
// padding —— 内边距
Text("Hello")
.padding() // 默认系统内边距(约 16pt)
.padding(20) // 所有方向 20pt
.padding(.horizontal, 20) // 水平方向 20pt
.padding(.top, 10) // 上方 10pt
// Spacer —— 弹性空间(≈ flex: 1)
HStack {
Text("Left")
Spacer() // 占据所有剩余空间
Text("Right")
}
// 结果:Left 在左端,Right 被推到右端
// 多个 Spacer 按比例分配
HStack {
Spacer() // 占 1/3
Text("Center")
Spacer() // 占 1/3
Text("Right")
Spacer() // 占 1/3
}
// Divider —— 分割线
VStack {
Text("Above")
Divider()
Text("Below")
}7.4 对齐(Alignment)
// VStack 的对齐(≈ align-items 在交叉轴方向)
VStack(alignment: .leading) { // 子视图左对齐
Text("Short")
Text("A much longer text")
}
// HStack 的对齐
HStack(alignment: .top) { // 子视图顶部对齐
Text("Short")
Text("Tall\nmultiline\ntext")
}
// ZStack 的对齐
ZStack(alignment: .topLeading) {
Color.blue.frame(width: 300, height: 300)
Text("Corner") // 在左上角
}
// frame 内的对齐
Text("Centered")
.frame(maxWidth: .infinity, alignment: .leading) // 文字在 frame 内左对齐7.5 GeometryReader 及其替代方案
GeometryReader 是 SwiftUI
中获取父视图几何信息的工具,类似
getBoundingClientRect()。
// GeometryReader:获取父视图的几何信息
GeometryReader { geometry in
VStack {
Text("Width: \(geometry.size.width)")
Text("Height: \(geometry.size.height)")
}
}
// ⚠️ GeometryReader 会尽可能撑满所有可用空间⚠️ 前端陷阱:GeometryReader 容易滥用。它会改变布局行为(撑满所有空间),而且代码可读性差。iOS 17+ 有更好的替代方案:
// ✅ 更好的替代方案
// 1. containerRelativeFrame —— 相对于容器的尺寸
Image("hero")
.resizable()
.containerRelativeFrame(.horizontal) { length, _ in
length * 0.8 // 占容器宽度的 80%
}
// 2. visualEffect —— 位置相关的视觉效果(不改变布局)
ScrollView {
ForEach(items) { item in
ItemCard(item: item)
.visualEffect { content, geometry in
let frame = geometry.frame(in: .scrollView)
let distance = min(0, frame.minY)
return content.opacity(1 + distance / 200)
}
}
}
// 3. onGeometryChange —— 观察几何变化(iOS 18+)
Text("Content")
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.height
} action: { newHeight in
print("Height changed to \(newHeight)")
}7.6 安全区域(Safe Area)
// 忽略安全区域(≈ CSS 的 env(safe-area-inset-*))
Color.blue
.ignoresSafeArea() // 延伸到屏幕边缘(包括刘海/灵动岛区域)
// 部分忽略
Color.blue
.ignoresSafeArea(edges: .top) // 只忽略顶部
// 安全区域内边距
ScrollView {
content
}
.contentMargins(.horizontal, 16) // 设置滚动内容的安全内边距
.safeAreaInset(edge: .bottom) {
BottomBar() // 在安全区域上方插入视图
}7.7 实战:聊天 App 布局
struct Message: Identifiable, Equatable {
let id = UUID()
let text: String
let isFromCurrentUser: Bool
let timestamp: Date
}
struct ChatView: View {
@State private var messages: [Message] = []
@State private var newMessage = ""
var body: some View {
VStack(spacing: 0) {
// 消息列表
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: 12) {
ForEach(messages) { message in
MessageBubble(message: message)
}
Color.clear.frame(height: 1).id("bottom")
}
.padding(.horizontal)
.padding(.top, 8)
}
.onChange(of: messages.count) { _, _ in
withAnimation {
proxy.scrollTo("bottom", anchor: .bottom)
}
}
}
Divider()
// 输入区域
HStack(spacing: 12) {
TextField("Message", text: $newMessage, axis: .vertical)
.lineLimit(1...5)
.padding(12)
.background(.gray.opacity(0.1))
.clipShape(.rect(cornerRadius: 20))
Button {
sendMessage()
} label: {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 32))
.foregroundStyle(newMessage.isEmpty ? .gray : .blue)
}
.disabled(newMessage.isEmpty)
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(.regularMaterial)
}
}
private func sendMessage() {
guard !newMessage.trimmingCharacters(in: .whitespaces).isEmpty else { return }
let message = Message(text: newMessage, isFromCurrentUser: true, timestamp: Date())
withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
messages.append(message)
}
newMessage = ""
}
}
struct MessageBubble: View {
let message: Message
var body: some View {
HStack {
if message.isFromCurrentUser { Spacer(minLength: 60) }
VStack(alignment: message.isFromCurrentUser ? .trailing : .leading, spacing: 4) {
Text(message.text)
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(
message.isFromCurrentUser ? .blue : .gray.opacity(0.15),
in: RoundedRectangle(cornerRadius: 18))
.foregroundStyle(message.isFromCurrentUser ? .white : .primary)
Text(message.timestamp, style: .time)
.font(.caption2)
.foregroundStyle(.secondary)
}
if !message.isFromCurrentUser { Spacer(minLength: 60) }
}
}
}7.8 实战:电商商品详情页
struct ProductDetailView: View {
let product: Product
@State private var selectedVariant: Variant?
@State private var quantity = 1
var body: some View {
ZStack(alignment: .bottom) {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
// 商品图片轮播
TabView {
ForEach(product.images, id: \.self) { url in
AsyncImage(url: url) { image in
image.resizable().scaledToFill()
} placeholder: {
Color.gray.opacity(0.2)
}
}
}
.frame(height: 350)
.tabViewStyle(.page)
VStack(alignment: .leading, spacing: 16) {
// 价格
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text("¥").font(.title3)
Text("\(product.price, specifier: "%.2f")")
.font(.system(size: 32, weight: .bold))
}
.foregroundStyle(.red)
// 商品名称
Text(product.name)
.font(.title3).fontWeight(.semibold)
// 规格选择
HStack(spacing: 10) {
ForEach(product.variants) { variant in
Button {
withAnimation(.spring(response: 0.3)) {
selectedVariant = variant
}
} label: {
Text(variant.name)
.font(.caption)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(
selectedVariant?.id == variant.id
? .blue : .gray.opacity(0.1))
.foregroundStyle(
selectedVariant?.id == variant.id
? .white : .primary)
.clipShape(.rect(cornerRadius: 8))
}
}
}
}
.padding()
}
}
.ignoresSafeArea(edges: .top)
// 固定在底部的购买栏
HStack(spacing: 12) {
Button { } label: {
Text("加入购物车")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(.orange)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: 12))
}
Button { } label: {
Text("立即购买")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(.red)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: 12))
}
}
.padding(.horizontal)
.padding(.vertical, 10)
.background(.regularMaterial)
}
}
}第 8 章:列表、集合与数据展示
8.1 ForEach 的本质
ForEach 是 SwiftUI
中最重要的视图之一,它解决了”如何从数据数组生成视图”的问题。
struct Person: Identifiable {
let id = UUID()
let name: String
}
let people = [
Person(name: "Alice"),
Person(name: "Bob"),
]
ForEach(people) { person in // Person 遵循 Identifiable,自动使用 id
Text(person.name)
}⚠️ 前端陷阱:不要用数组索引作为 id——和 React 的
key={index}
一样,会导致动画错乱和状态丢失。始终使用稳定的唯一标识。
8.2 List 的完整用法
struct ContentView: View {
@State private var items = Item.samples
@State private var selection: Set<Item.ID> = []
var body: some View {
List(selection: $selection) {
ForEach(items) { item in
ItemRow(item: item)
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.listRowSeparator(.hidden)
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
.onMove { from, to in
items.move(fromOffsets: from, toOffset: to)
}
}
.listStyle(.plain)
.refreshable { // 下拉刷新
await loadMoreItems()
}
}
}List 的样式:
| 样式 | 外观 | 适用场景 |
|---|---|---|
.automatic |
系统默认 | 大多数情况 |
.plain |
无分组背景 | 自定义背景的列表 |
.grouped |
分组灰色背景 | 偏好设置 |
.insetGrouped |
圆角分组 | iOS 风格设置页 |
8.3 空状态与加载状态
struct ContentView: View {
@State private var items: [Item] = []
@State private var isLoading = true
@State private var errorMessage: String?
var body: some View {
Group {
if isLoading {
ProgressView("Loading...")
} else if let errorMessage {
ContentUnavailableView(
"Error", systemImage: "wifi.slash",
description: Text(errorMessage))
} else if items.isEmpty {
ContentUnavailableView(
"No Items", systemImage: "tray",
description: Text("Your items will appear here."))
} else {
List(items) { item in ItemRow(item: item) }
}
}
.task {
do {
isLoading = true
items = try await fetchItems()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
}8.4 Lazy 容器
// LazyVStack:只创建屏幕上可见的视图(≈ 虚拟列表)
ScrollView {
LazyVStack(spacing: 16) {
ForEach(0..<10000) { i in
Text("Item \(i)")
.frame(height: 100)
}
}
}
// 不是一次性创建 10000 个 Text,而是只创建屏幕上可见的约 10-15 个⚠️ 前端陷阱:普通 VStack 会一次性创建所有子视图。如果数据量超过约 20 项,就必须使用 LazyVStack。
8.5 Grid 网格布局
struct PhotoGrid: View {
let photos: [Photo]
let columns = [
GridItem(.adaptive(minimum: 150), spacing: 8)
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(photos) { photo in
AsyncImage(url: photo.url) { image in
image.resizable().aspectRatio(contentMode: .fill)
} placeholder: {
Color.gray
}
.frame(height: 150)
.clipShape(.rect(cornerRadius: 8))
}
}
.padding(8)
}
}
}
// GridItem(.adaptive(minimum: 150)) 表示:
// 每列最小宽度 150pt,自动计算能放多少列8.6 实战:搜索与防抖(Debounce)
@Observable
@MainActor
final class SearchViewModel {
var query = ""
var results: [SearchResult] = []
var isLoading = false
var searchTask: Task<Void, Never>?
func search() async {
searchTask?.cancel() // 取消上一次搜索
let query = query.trimmingCharacters(in: .whitespaces)
guard !query.isEmpty else { results = []; return }
searchTask = Task {
try? await Task.sleep(for: .milliseconds(300)) // 防抖 300ms
guard !Task.isCancelled else { return }
isLoading = true
defer { isLoading = false }
do {
let url = URL(string: "https://api.example.com/search?q=\(query)")!
let (data, _) = try await URLSession.shared.data(from: url)
results = try JSONDecoder().decode([SearchResult].self, from: data)
} catch {
if !Task.isCancelled { results = [] }
}
}
}
}
struct SearchView: View {
@State private var viewModel = SearchViewModel()
@State private var searchText = ""
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView()
} else if viewModel.results.isEmpty && !searchText.isEmpty {
ContentUnavailableView.search(text: searchText)
} else {
List(viewModel.results) { result in
SearchResultRow(result: result)
}
}
}
.searchable(text: $searchText, prompt: "Search items...")
.onChange(of: searchText) { _, newValue in
viewModel.query = newValue
Task { await viewModel.search() }
}
}
}
}8.7 实战:无限滚动加载(分页)
@Observable
@MainActor
final class PaginatedListViewModel {
var items: [Item] = []
var isLoading = false
var hasMore = true
private var currentPage = 0
private let pageSize = 20
func loadMore() async {
guard !isLoading, hasMore else { return }
isLoading = true
defer { isLoading = false }
do {
let newItems = try await fetchPage(currentPage, pageSize: pageSize)
items.append(contentsOf: newItems)
currentPage += 1
hasMore = newItems.count == pageSize
} catch { }
}
func refresh() async {
currentPage = 0; items = []; hasMore = true
await loadMore()
}
}
struct PaginatedListView: View {
@State private var viewModel = PaginatedListViewModel()
var body: some View {
List {
ForEach(viewModel.items) { item in
ItemRow(item: item)
.onAppear {
// 当倒数第 5 个元素出现时,提前加载下一页
if item.id == viewModel.items.dropLast(5).last?.id {
Task { await viewModel.loadMore() }
}
}
}
if viewModel.isLoading {
HStack { Spacer(); ProgressView(); Spacer() }
.listRowSeparator(.hidden)
}
}
.refreshable { await viewModel.refresh() }
.task { await viewModel.loadMore() }
}
}8.8 实战:滑动操作(Swipe Actions)
struct InboxListView: View {
@State private var messages: [Message] = Message.samples
var body: some View {
List {
ForEach(messages) { message in
MessageRow(message: message)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deleteMessage(message)
} label: {
Label("Delete", systemImage: "trash")
}
Button {
archiveMessage(message)
} label: {
Label("Archive", systemImage: "archivebox")
}
.tint(.orange)
}
.swipeActions(edge: .leading) {
Button {
toggleRead(message)
} label: {
Label(message.isRead ? "Unread" : "Read",
systemImage: message.isRead ? "envelope.badge" : "envelope.open")
}
.tint(.blue)
}
}
}
}
}8.9 实战:可展开的折叠列表
struct FAQView: View {
let faqs: [FAQItem]
var body: some View {
List {
ForEach(faqs) { faq in
DisclosureGroup {
Text(faq.answer)
.font(.body)
.foregroundStyle(.secondary)
.padding(.vertical, 8)
} label: {
Text(faq.question)
.font(.headline)
}
}
}
}
}8.10 实战:多选与编辑模式
struct EditableListView: View {
@State private var items: [Item] = Item.samples
@State private var selectedIDs: Set<Item.ID> = []
@State private var editMode: EditMode = .inactive
var body: some View {
NavigationStack {
List(selection: $selectedIDs) {
ForEach(items) { item in
ItemRow(item: item)
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
.navigationTitle("Items")
.environment(\.editMode, $editMode)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if editMode == .active {
HStack {
Button("Delete (\(selectedIDs.count))") {
items.removeAll { selectedIDs.contains($0.id) }
selectedIDs.removeAll()
editMode = .inactive
}
.disabled(selectedIDs.isEmpty)
Button("Done") { editMode = .inactive }
}
} else {
Button("Edit") { editMode = .active }
}
}
}
}
}
}第 9 章:导航架构
9.1 NavigationStack —— 类型安全的导航
struct ContentView: View {
let items: [Item]
var body: some View {
NavigationStack {
List(items) { item in
NavigationLink(value: item) {
ItemRow(item: item)
}
}
.navigationTitle("Items")
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
}
}前端类比:≈ React Navigation 的 Stack Navigator,但更类型安全。
9.2 编程式导航
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack(spacing: 20) {
Button("Go to Profile") {
path.append(Route.profile) // ≈ navigation.navigate('Profile')
}
}
.navigationDestination(for: Route.self) { route in
switch route {
case .profile: ProfileView()
case .detail(let id): DetailView(id: id)
}
}
}
}
}
enum Route: Hashable {
case profile
case detail(id: Int)
}9.3 Sheet 的完整模式
// 多个 Sheet —— 使用枚举管理
struct ContentView: View {
enum Sheet: Identifiable {
case add, settings
var id: String { "\(self)" }
}
@State private var sheet: Sheet?
var body: some View {
List {
Button("Add Item") { sheet = .add }
}
.sheet(item: $sheet) { sheet in
switch sheet {
case .add: AddItemView()
case .settings: SettingsView()
}
}
}
}Sheet 自己处理 dismiss:
struct EditItemView: View {
@Environment(\.dismiss) private var dismiss // ≈ navigation.goBack()
var body: some View {
NavigationStack {
Form { /* ... */ }
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") { dismiss() }
}
}
}
}
}⚠️ 前端陷阱:不要从父视图传 onSave/onCancel 回调。Sheet 自己处理自己的逻辑。
9.4 TabView
struct MainTabView: View {
@State private var selectedTab: Tab = .home
var body: some View {
TabView(selection: $selectedTab) {
Tab("Home", systemImage: "house", value: .home) { HomeView() }
Tab("Search", systemImage: "magnifyingglass", value: .search) { SearchView() }
Tab("Profile", systemImage: "person", value: .profile) { ProfileView() }
}
}
}
enum Tab: Hashable { case home, search, profile }⚠️ 前端陷阱:每个 Tab 应该有独立的 NavigationStack。
9.5 iOS 26 TabView 新特性
TabView { /* tabs */ }
.tabBarMinimizeBehavior(.onScrollDown) // 滚动时自动收起
.tabViewBottomAccessory { NowPlayingBar() } // Tab 栏上方常驻控件
// 专用搜索 Tab
TabView {
Tab("Home", systemImage: "house") { HomeView() }
Tab(role: .search) { SearchResultsView() }
}
.searchable(text: $searchText)9.6 实战:认证流程
@Observable @MainActor
final class AuthManager {
var isLoggedIn = false
var currentUser: User?
func login(email: String, password: String) async throws { /* ... */ }
func logout() { isLoggedIn = false; currentUser = nil }
}
@main
struct MyApp: App {
@State private var authManager = AuthManager()
var body: some Scene {
WindowGroup {
Group {
if authManager.isLoggedIn { MainTabView() }
else { LoginView() }
}
.environment(authManager)
}
}
}9.7 实战:深层链接
.onOpenURL { url in
// 解析 myapp://post/123
guard let host = URLComponents(url: url, resolvingAgainstBaseURL: true)?.host else { return }
switch host {
case "post":
let id = url.pathComponents.dropFirst().first ?? ""
navigationPath.append(AppRoute.post(id: id))
default: break
}
}第 10 章:动画系统
10.1 隐式动画 vs 显式动画
// 隐式动画:动画是视图的固有属性
RoundedRectangle(cornerRadius: 12)
.fill(isExpanded ? .blue : .gray)
.frame(width: isExpanded ? 200 : 100, height: 100)
.animation(.spring(response: 0.5), value: isExpanded)
// 显式动画:动画是用户操作的响应
Button("Toggle") {
withAnimation(.spring(response: 0.5)) { isExpanded.toggle() }
}⚠️ 前端陷阱:必须用
.animation(_:value:) 带 value 参数的版本。
10.2 转场动画
if showDetail {
DetailCard()
.transition(.scale.combined(with: .opacity))
}
.animation(.spring, value: showDetail) // 动画放在条件外面!⚠️ 前端陷阱:动画修饰符必须放在条件语句外面。
10.3 动画性能
// ✅ 高性能:GPU 层面变换
.scaleEffect(isActive ? 1.5 : 1.0)
.offset(x: isActive ? 50 : 0)
.opacity(isActive ? 1 : 0.5)
// ❌ 低性能:触发布局
.frame(width: isActive ? 200 : 100)前端类比:≈ CSS 的
transform/opacity vs
width/height。
10.4 matchedGeometryEffect(Hero 动画)
struct HeroView: View {
@Namespace private var namespace
@State private var showDetail = false
var body: some View {
ZStack {
if !showDetail {
Image("hero")
.matchedGeometryEffect(id: "hero", in: namespace)
.onTapGesture {
withAnimation(.spring) { showDetail = true }
}
}
if showDetail {
DetailView()
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
}
}10.5 @Animatable 宏(iOS 26)
@Animatable
struct LoadingArc: Shape {
var startAngle: Angle
var endAngle: Angle
@AnimatableIgnored var drawClockwise: Bool
func path(in rect: CGRect) -> Path { /* ... */ }
}10.6 实战:骨架屏
struct SkeletonRow: View {
@State private var isAnimating = false
var body: some View {
HStack(spacing: 12) {
Circle().fill(.gray.opacity(0.2)).frame(width: 40, height: 40)
VStack(alignment: .leading, spacing: 8) {
RoundedRectangle(cornerRadius: 4).fill(.gray.opacity(0.2)).frame(height: 16)
RoundedRectangle(cornerRadius: 4).fill(.gray.opacity(0.2)).frame(width: 200, height: 12)
}
}
.overlay {
Color.white.opacity(isAnimating ? 0 : 0.3)
.animation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true), value: isAnimating)
}
.onAppear { isAnimating = true }
}
}第 11 章:Swift 6 并发编程
11.1 async/await
func fetchData() async throws -> Data {
let url = URL(string: "https://api.example.com")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}前端类比:≈ JS 的 async/await,但 Swift
的 await 不阻塞线程。
11.2 @MainActor 与默认隔离(Swift 6.2)
Swift 6.2 新语言模式:View 协议自动 @MainActor
隔离,大多数情况下不需要显式标记。
⚠️ 前端陷阱:UI 更新必须在主线程(类似浏览器主线程)。
11.3 Task 与 .task
struct MyView: View {
@State private var data: [Item] = []
var body: some View {
List(data) { item in Text(item.name) }
.task { // 绑定视图生命周期:出现时启动,消失时取消
data = (try? await fetchItems()) ?? []
}
}
}
// 手动 Task
Button("Save") {
Task { await saveToServer() } // 在同步回调中桥接到异步
}11.4 SwiftUI 并发最佳实践(WWDC 25 Session 266)
⚠️ 重要规则: - 同步 action 回调(Button 等)不要用
async,用 Task 包裹 - 后台线程
API(Shape.path、visualEffect、Layout)需要
Sendable 闭包 - 推荐架构:View (同步) → State → Model (异步) →
State 更新 → View 更新
// 后台线程闭包中访问 @MainActor 属性,用捕获列表复制值
.visualEffect { [pulse] content, _ in
content.blur(radius: pulse ? 2 : 0)
}11.5 常见并发错误速查
// 错误 1:主线程隔离问题
// 解决:标记方法为 @MainActor
@MainActor func updateState() { self.items = newItems }
// 错误 2:不能在同步上下文中调用异步函数
// 解决:用 Task 包裹
Button("Load") { Task { await loadData() } }第 12 章:网络请求与数据层
12.1 URLSession
// GET 请求
func fetchItems() async throws -> [Item] {
let url = URL(string: "https://api.example.com/items")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Item].self, from: data)
}
// POST 请求
func createItem(_ item: Item) async throws -> Item {
let url = URL(string: "https://api.example.com/items")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(item)
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(Item.self, from: data)
}前端类比:≈ fetch API,但更底层。
12.2 Codable —— JSON 解析
struct Item: Codable, Identifiable {
let id: Int
let title: String
let price: Double
let createdAt: Date
enum CodingKeys: String, CodingKey {
case id, title, price
case createdAt = "created_at" // JSON key 映射
}
}
// 解码策略
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // snake_case → camelCase
decoder.dateDecodingStrategy = .iso860112.3 完整的 ViewModel 模式
@Observable @MainActor
final class FeedViewModel {
var items: [Item] = []
var isLoading = false
var hasMore = true
var errorMessage: String?
private var currentPage = 0
private let pageSize = 20
func loadItems() async {
guard !isLoading else { return }
isLoading = true; errorMessage = nil
do {
let newItems = try await fetchPage(0, pageSize: pageSize)
items = newItems
currentPage = 1
hasMore = newItems.count == pageSize
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
func loadMore() async {
guard !isLoading, hasMore else { return }
isLoading = true
do {
let newItems = try await fetchPage(currentPage, pageSize: pageSize)
items.append(contentsOf: newItems)
currentPage += 1
hasMore = newItems.count == pageSize
} catch {
// 加载更多失败时保留已有数据,不覆盖
}
isLoading = false
}
func refresh() async {
// 先尝试刷新,成功后才替换旧数据
do {
let newItems = try await fetchPage(0, pageSize: pageSize)
items = newItems
currentPage = 1
hasMore = newItems.count == pageSize
errorMessage = nil
} catch {
errorMessage = error.localizedDescription
// 刷新失败时保留旧数据
}
}
}
struct FeedView: View {
@State private var viewModel = FeedViewModel()
var body: some View {
Group {
if viewModel.isLoading && viewModel.items.isEmpty {
ProgressView()
} else if let error = viewModel.errorMessage, viewModel.items.isEmpty {
ContentUnavailableView("Error", systemImage: "wifi.slash", description: Text(error))
} else {
List(viewModel.items) { item in
ItemRow(item: item)
.onAppear {
if item.id == viewModel.items.dropLast(5).last?.id {
Task { await viewModel.loadMore() }
}
}
}
.refreshable { await viewModel.refresh() }
}
}
.task { await viewModel.loadItems() }
}
}第 13 章:数据持久化
13.1 @AppStorage —— 简单的键值存储
struct SettingsView: View {
@AppStorage("isDarkMode") private var isDarkMode = false
@AppStorage("username") private var username = ""
var body: some View {
Form {
Toggle("Dark Mode", isOn: $isDarkMode)
TextField("Username", text: $username)
}
}
}⚠️ 前端陷阱:@AppStorage 底层是
UserDefaults,明文存储。不要存储
token、密码等敏感信息。
13.2 SwiftData —— 现代持久化框架
import SwiftData
@Model
final class TodoItem {
var title: String
var isCompleted: Bool
var createdAt: Date
init(title: String) {
self.title = title
self.isCompleted = false
self.createdAt = Date()
}
}
// App 入口
@main
struct TodoApp: App {
let container: ModelContainer
init() {
do {
container = try ModelContainer(for: TodoItem.self)
} catch {
// 迁移失败、磁盘满、权限问题等
fatalError("Failed to create ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup { TodoListView() }
.modelContainer(container)
}
}
// 视图中使用
struct TodoListView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \TodoItem.createdAt, order: .reverse) private var items: [TodoItem]
@State private var newItemTitle = ""
var body: some View {
NavigationStack {
List {
ForEach(items) { item in
HStack {
Button { item.isCompleted.toggle() } label: {
Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
}
Text(item.title)
}
}
.onDelete { indexSet in
for index in indexSet { modelContext.delete(items[index]) }
}
}
.toolbar {
HStack {
TextField("New task", text: $newItemTitle)
Button("Add") {
modelContext.insert(TodoItem(title: newItemTitle))
newItemTitle = ""
}
}
}
}
}
}13.3 Keychain —— 安全存储
存储 token、密码等敏感信息必须用 Keychain。
// 使用第三方库 KeychainAccess(推荐)
import KeychainAccess
let keychain = Keychain(service: "com.yourapp")
// 存储
keychain["authToken"] = "eyJhbGciOiJIUzI1NiIs..."
// 读取
let token = keychain["authToken"]
// 删除
keychain["authToken"] = nil⚠️ 前端陷阱:浏览器没有 Keychain 概念。在 iOS
中,用户 token 必须用 Keychain 存储,不能用
@AppStorage。
13.4 文件系统存储
// 缓存目录(系统可能清理)
let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
// 文档目录(用户可见,不会被清理)
let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
// 存储图片
let imageURL = docsDir.appendingPathComponent("avatar.jpg")
try imageData.write(to: imageURL)
// 读取
let data = try Data(contentsOf: imageURL)第 14 章:性能优化
14.1 提取子视图以减少更新范围
// ❌ 问题:一个状态变化,整个大视图重新执行 body
struct BigView: View {
@State private var count = 0
@State private var name = ""
var body: some View {
VStack {
CounterView(count: $count)
NameView(name: $name)
}
}
}
// ✅ 解决:状态已经分散到子视图中
struct CounterView: View {
@Binding var count: Int
var body: some View { Button("Count: \(count)") { count += 1 } }
}前端类比:≈ React.memo + 拆分组件。
14.2 传递最小依赖
// ❌ 传整个 model
struct ItemRow: View {
@Environment(AppModel.self) private var model
let item: Item
var body: some View {
Text(item.name).foregroundStyle(model.theme.primaryColor)
}
}
// ✅ 只传需要的值
struct ItemRow: View {
let item: Item
let themeColor: Color
var body: some View {
Text(item.name).foregroundStyle(themeColor)
}
}14.3 调试工具
var body: some View {
#if DEBUG
let _ = Self._printChanges() // 打印更新原因
#endif
// 视图代码
}14.4 iOS 26 性能改进
- macOS List:6x 加载速度,16x 更新速度
- 滚动性能优化
- 嵌套 ScrollView + LazyVStack 正确懒加载
- SwiftUI Performance Instrument:查看 body 执行时间、状态变化因果图
14.5 常见性能陷阱
| 陷阱 | 解决 |
|---|---|
| 在 body 中创建对象 | 用静态属性 |
| 在 body 中排序/过滤 | 预计算 |
| 传整个 model 给行视图 | 只传需要的值 |
| scroll 回调中频繁更新状态 | 加阈值判断 |
| 存储派生状态 | 用计算属性 |
第 15 章:无障碍与国际化
15.1 Dynamic Type
// 使用系统字体样式(自动适配 Dynamic Type)
Text("Title").font(.title)
Text("Body").font(.body)
// 自定义字体也要适配
Text("Custom")
.font(.custom("SourceSerif", size: 17, relativeTo: .body))
// 非文字值也适配
@ScaledMetric private var iconSize = 24.0
Image(systemName: "star").font(.system(size: iconSize))15.2 VoiceOver
// 组合元素
HStack {
Image(systemName: "star.fill")
Text("Favorites")
}
.accessibilityElement(children: .combine) // 读作一个元素
// 隐藏装饰性元素
Image(systemName: "sparkles").accessibilityHidden(true)
// 自定义标签
Button(action: {}) { Image(systemName: "trash") }
.accessibilityLabel("Delete item")15.3 用 Button 而不是 onTapGesture
// ✅ Button 自带完整的无障碍支持
Button("Save") { save() }
// ❌ onTapGesture 不支持 VoiceOver、键盘、Switch Control
Text("Save").onTapGesture { save() }15.4 国际化
// iOS 26 String Catalogs:类型安全的本地化符号
// 自动生成翻译注释(使用设备端模型分析字符串上下文)第 16 章:从开发到上架
16.1 开发流程
Xcode 开发 → 模拟器测试 → 真机测试 → Archive → 上传 → TestFlight → 提交审核 → 上架
16.2 App Store Connect 配置
- Bundle Identifier:App 唯一标识(如
com.yourcompany.yourapp) - App Icon:1024x1024 PNG
- 截图:至少 6.5 英寸和 5.5 英寸各一套
- 隐私政策 URL:收集数据必须提供
- App 描述:简短有吸引力
16.3 App Icon(iOS 26 更新)
iOS 26 的 Icon Composer 支持多层图标设计: - Dark 模式变体 - Tinted 模式变体 - watchOS 变体 - 材质效果、模糊、阴影、高光、半透明
16.4 版本管理
版本号:主版本.次版本.修订版本 (如 1.0.0)
Build 号:每次上传递增 (如 1, 2, 3, ...)
附录 A:React → SwiftUI 速查表
A.1 组件模式
| React | SwiftUI |
|---|---|
<Component /> |
Component() |
children |
@ViewBuilder 闭包参数 |
props |
let 参数 / @Binding |
defaultProps |
init 默认参数 |
React.memo |
提取独立 View struct |
A.2 Hooks → 属性包装器
| React Hook | SwiftUI | 说明 |
|---|---|---|
useState |
@State |
视图私有状态 |
useContext |
@Environment |
全局共享状态 |
useEffect(fn, []) |
.task { } |
视图出现时执行 |
useEffect(fn, [dep]) |
.onChange(of: dep) { } |
依赖变化时执行 |
useReducer |
@Observable + 方法 |
复杂状态逻辑 |
useRef |
没有直接对应 | 用 @State 或 FocusState |
useMemo |
计算属性 var x: Int { } |
派生值 |
useCallback |
闭包 | 函数引用 |
A.3 生命周期
| React | SwiftUI |
|---|---|
componentDidMount |
.onAppear { } / .task { } |
componentWillUnmount |
.onDisappear { } |
componentDidUpdate |
.onChange(of: value) { } |
A.4 样式
| React (CSS-in-JS) | SwiftUI |
|---|---|
style={{ color: 'blue' }} |
.foregroundStyle(.blue) |
style={{ fontSize: 17 }} |
.font(.system(size: 17)) |
style={{ padding: 10 }} |
.padding(10) |
style={{ margin: 10 }} |
用 Spacer 或 padding |
style={{ borderRadius: 8 }} |
.clipShape(.rect(cornerRadius: 8)) |
style={{ opacity: 0.5 }} |
.opacity(0.5) |
className="active" |
条件修饰符 |
A.5 导航
| React Navigation | SwiftUI |
|---|---|
Stack.Navigator |
NavigationStack |
navigation.navigate('Route') |
path.append(Route) |
navigation.goBack() |
path.removeLast() / dismiss() |
Tab.Navigator |
TabView |
Drawer.Navigator |
NavigationSplitView |
附录 B:常见错误与调试
B.1 编译错误
| 错误信息 | 常见原因 | 解决方法 |
|---|---|---|
Type 'X' does not conform to protocol 'View' |
缺少 body | 实现 var body: some View |
Cannot find 'X' in scope |
未定义的变量 | 检查拼写和导入 |
The compiler is unable to type-check... |
视图太复杂 | 提取子视图 |
Property wrappers conflict with @Observable |
@Observable 中用了 @AppStorage | 加 @ObservationIgnored |
B.2 运行时错误
| 现象 | 常见原因 | 解决方法 |
|---|---|---|
| 视图不更新 | 不是 @State 或 @Observable | 检查属性包装器 |
| 传值不生效 | 用 @State 接收了外部值 | 改用 let |
| 列表项错乱 | ForEach 用了不稳定的 id | 用稳定的唯一 id |
| 内存占用高 | 用了 VStack 而不是 LazyVStack | 换用 Lazy 容器 |
| 动画不生效 | animation 修饰符在条件语句内 | 移到外面 |
B.3 调试技巧
// 1. 打印视图更新原因
var body: some View {
let _ = Self._printChanges()
// 视图代码
}
// 2. 打印状态变化
.onChange(of: someValue) { old, new in
print("Changed from \(old) to \(new)")
}附录 C:术语表
| 术语 | 英文 | 解释 | 前端类比 |
|---|---|---|---|
| 视图 | View | SwiftUI 的 UI 组件 | React 组件 |
| 修饰符 | Modifier | 链式调用,返回新视图 | CSS 样式 |
| 状态 | State | 驱动 UI 更新的数据 | useState |
| 属性包装器 | Property Wrapper | @ 开头的语法糖 |
Hook |
| 绑定 | Binding | 双向引用(读和写) | 受控组件 |
| 环境 | Environment | 全局传递的数据 | Context |
| 场景 | Scene | App 的顶层容器 | - |
| 安全区域 | Safe Area | 不被刘海遮挡的区域 | safe-area-inset |
| SF Symbols | SF Symbols | 6000+ 免费图标 | Font Awesome |
| 归档 | Archive | 打包为 .ipa 文件 | npm run build |
| 主线程 | Main Actor | UI 线程 | 浏览器主线程 |
| 挂起点 | Suspension Point | await 的位置 | await |
| 标识 | Identity | ForEach 中的唯一值 | React key |