SwiftUI 从零到一

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 开发全景图

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 年的今天,这个问题已经很简单了:

0.3 你需要一台 Mac

这是 iOS 开发唯一的硬件门槛。Xcode 只能在 macOS 上运行。最低配置建议:

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 创建你的第一个项目

  1. 打开 Xcode → File → New → Project
  2. 选择 iOS → App
  3. 填写:
    • Product Name: MyFirstApp
    • Interface: SwiftUI
    • Language: Swift
  4. 点击 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 画布。它能:

这是 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 = 42Intlet x = 42.0Double。两者不能直接混用运算。

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,只有 nilnil 不是值,是”没有值”。你不能把 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 个概念

  1. let vs var — 常量 vs 变量
  2. Optional(?if let — 处理可能为空的值
  3. Struct vs Class — 值类型 vs 引用类型
  4. 闭包(Closure) — 匿名函数,尾随闭包语法
  5. 属性包装器(@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 创建项目

  1. 打开 Xcode → File → New → Project
  2. 选择 iOS → App
  3. 填写:
    • Product Name: MyFirstApp
    • Interface: SwiftUI
    • Language: Swift
  4. 点击 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()
}

逐行解释

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 中 paddingbackground 的关系取决于 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 的关键规则

  1. 标记类为 @MainActor:UI 相关状态必须在主线程操作

  2. @Observable 类中使用属性包装器必须加 @ObservationIgnored

@Observable
@MainActor
final class SettingsModel {
    // ✅ 必须加 @ObservationIgnored
    @ObservationIgnored @AppStorage("isDarkMode") var isDarkMode = false

    // 普通属性不需要
    var isLoading = false
}
  1. 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 状态管理性能

⚠️ 重要区别

这意味着如果你有一个 @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 章:导航架构

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.pathvisualEffectLayout)需要 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 = .iso8601

12.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 性能改进

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 配置

  1. Bundle Identifier:App 唯一标识(如 com.yourcompany.yourapp
  2. App Icon:1024x1024 PNG
  3. 截图:至少 6.5 英寸和 5.5 英寸各一套
  4. 隐私政策 URL:收集数据必须提供
  5. 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 没有直接对应 @StateFocusState
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