iOS 开发手册IOS DEVELOPMENT

CHAPTER 03声明式 UI · DECLARATIVE

SwiftUI

用声明"界面应该是什么样"取代命令式"一步步改界面"。状态驱动视图,视图是状态的函数。本章覆盖状态管理、布局、动画与互操作。

开始阅读 难度进阶 先修Swift + 并发 约3小时
03.1

声明式心智模型

UIKit 是"拿到一个 label,设置它的文字";SwiftUI 是"如果 count 是 5,就显示 5"。你描述界面与状态的对应关系,框架负责在状态变化时高效地刷新。

视图是轻量的值

SwiftUI 的 View 是结构体(值类型),body 每次被调用都重建一份视图描述。它不是"屏幕上的控件",而是"当前应当如何展示"的说明书。框架用差异算法比较前后两份描述,只更新真正变化的部分。

03.2

View 协议与视图

所有界面元素都遵循 View 协议,要求实现一个 body 属性,返回 some View。

import SwiftUI

struct WelcomeView: View {
    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: "star.fill")
                .font(.largeTitle)
                .foregroundStyle(.yellow)
            Text("欢迎")
                .font(.title.bold())
            Button("开始") { print("tapped") }
        }
        .padding()
    }
}
03.3

状态管理

状态属性包装器决定"这块数据存在哪、谁拥有、变化了怎么通知视图"。选错会导致视图不刷新或数据串台。

包装器归属用途
@State视图私有视图自己拥有、会变化的值(如输入框文字、开关)
@Binding引用他人子视图读写父视图的 @State,不拥有数据
@StateObject / @ObservedObject引用类型对象监听 ObservableObject(旧范式)
@Environment环境注入主题、尺寸类别等跨层级共享值
@Observable(iOS 17+)可观察对象新范式:直接标记 class,字段变化自动驱动视图
// 父视图拥有状态
struct Parent: View {
    @State private var count = 0
    var body: some View {
        VStack {
            Text("计数:\(count)")
            CounterButton(count: $count)   // $ 传绑定
        }
    }
}

// 子视图通过 Binding 读写
struct CounterButton: View {
    @Binding var count: Int
    var body: some View {
        Button("加一") { count += 1 }
    }
}
03.4

数据流向(@Observable 新范式)

iOS 17 起官方主推 @Observable 宏:给 class 加注解后,其被读取的字段变化会自动触发依赖它的视图刷新,不再需要 @Published。

import Observation

@Observable
final class CartModel {
    var items: [Product] = []
    var total: Int { items.reduce(0) { $0 + $1.price } }
    func add(_ p: Product) { items.append(p) }
}

struct CartView: View {
    @State private var model = CartModel()   // 持有可观察对象
    var body: some View {
        List(model.items) { item in
            Text(item.name)
        }
        .onTapGesture { model.add(sample) }   // 变化自动刷新列表
    }
}
旧范式对照

旧写法用 ObservableObject + @Published,视图侧用 @StateObject/@ObservedObject。新 @Observable 更简洁,但仅 iOS 17+ 可用;维护老项目时仍需理解旧范式。

03.5

布局系统

SwiftUI 布局是"父提议尺寸 → 子回报尺寸 → 父定位"的三步协议。常用栈(VStack/HStack/ZStack)、frame、Spacer 与 GeometryReader 表达。

VStack(alignment: .leading, spacing: 12) {
    Text("标题").font(.headline)
    Text("副标题").foregroundStyle(.secondary)
    Spacer()                       // 把下方内容推到底部
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)

// GeometryReader 读取可用空间
GeometryReader { proxy in
    Rectangle().fill(.blue)
        .frame(width: proxy.size.width * 0.5)
}
03.6

列表与导航

现代导航用 NavigationStack + navigationDestination,列表用 List 配合 ForEach 与可识别元素。

NavigationStack {
    List(products) { product in
        NavigationLink(value: product) {
            Text(product.name)
        }
    }
    .navigationTitle("商品")
    .navigationDestination(for: Product.self) { p in
        ProductDetailView(product: p)
    }
}
可识别(Identifiable)

列表元素需可唯一标识:让模型遵循 Identifiable(提供 id),或传 id: \.id 键路径。错误使用会引起列表复用混乱与动画异常。

03.7

动画与转场

动画在 SwiftUI 里是"声明状态变化如何过渡",而不是"手动驱动每一帧"。withAnimation 让状态变化带上过渡。

@State private var expanded = false

Button("展开") { withAnimation(.spring) { expanded.toggle() } }
if expanded {
    DetailView().transition(.opacity.combined(with: .move(edge: .top)))
}
03.8

与 UIKit 互操作

当 SwiftUI 缺少某个控件(如复杂文本编辑器、第三方 UIKit 组件)时,用 UIViewRepresentable / UIViewControllerRepresentable 包一层。

struct ActivityIndicator: UIViewRepresentable {
    var animating: Bool
    func makeUIView(context: Context) -> UIActivityIndicatorView {
        UIActivityIndicatorView(style: .large)
    }
    func updateUIView(_ view: UIActivityIndicatorView, context: Context) {
        animating ? view.startAnimating() : view.stopAnimating()
    }
}
03.9

常见陷阱与 FAQ

为什么改了数据视图却不刷新?

多半是状态归属错了:把应放在 @Observable 对象或 @State 里的数据放成了普通 let/var,或子视图拿到了值的副本而非绑定。确保被观察的字段确实是可观察对象的属性,且视图读取了它。

@StateObject 和 @ObservedObject 怎么分?

谁创建对象谁用 @StateObject(拥有生命周期);从外部传入则视图用 @ObservedObject(不拥有)。用错可能导致对象被反复重建。新项目直接用 @Observable 可避开这套区分。

body 里能写副作用吗(网络请求等)?

不应在 body 里做副作用,因为 body 可能被频繁重建。用 .task { } 修饰符(视图出现时启动 Task,消失时自动取消)来触发异步工作。

03.10

实战练习与资源

延伸资源