Chapter 02

SwiftUI 基础:View 与修饰符

理解声明式 UI 编程思想,掌握 View 协议与修饰符链的核心机制

2.1 声明式 UI vs 命令式 UI

传统的 UIKit 是命令式编程:你告诉系统"怎么做"——先创建视图,设置属性,添加到父视图,注册回调……每一步都需要明确指令。

SwiftUI 是声明式编程:你只需描述"界面应该是什么样子",框架负责计算差异并更新 UI。当数据变化时,SwiftUI 自动重新渲染相关视图。

UIKit(命令式)

let label = UILabel()
label.text = "Hello"
label.font = .systemFont(ofSize: 24)
label.textColor = .blue
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
// ...添加约束...

SwiftUI(声明式)

Text("Hello")
    .font(.system(size: 24))
    .foregroundColor(.blue)
// 就这些!

2.2 View 协议

在 SwiftUI 中,一切皆 ViewView 是一个协议,要求实现一个计算属性 body,返回视图的内容描述。

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI!")
    }
}
ℹ️
some View 是什么?

some View 是"不透明类型"(Opaque Type),表示"某个具体的 View 类型,但调用者不需要知道是哪种"。这让 SwiftUI 能在编译时确定视图类型,提升性能。

2.3 基础视图组件

Text — 文本

Text("普通文本")
Text("**粗体** 和 *斜体*")     // Markdown 支持
Text(Date(), style: .time)    // 动态时间显示
Text("很长的文本...")
    .lineLimit(2)              // 最多2行
    .truncationMode(.middle)   // 中间截断

Image — 图片

Image("photo")              // Assets 中的图片
    .resizable()
    .scaledToFit()
    .frame(width: 200)

Image(systemName: "heart.fill")  // SF Symbols 图标
    .font(.system(size: 30))
    .foregroundColor(.red)

Button — 按钮

Button("点我") {
    print("被点击了")
}

Button {
    // 操作
} label: {
    HStack {
        Image(systemName: "plus")
        Text("添加")
    }
}

TextField — 输入框

@State private var inputText = ""

TextField("请输入...", text: $inputText)
    .textFieldStyle(.roundedBorder)
    .padding()

2.4 修饰符(Modifiers)

修饰符是 SwiftUI 的核心机制之一。每个修饰符接收一个视图,返回一个新的视图(装饰器模式)。多个修饰符可以链式调用,顺序非常重要

Text("标题")
    .font(.largeTitle)           // 字体大小
    .fontWeight(.bold)           // 字重
    .foregroundColor(.white)     // 文字颜色
    .padding()                   // 内边距(所有方向)
    .padding(.horizontal, 20)  // 水平方向额外内边距
    .background(.blue)           // 背景色
    .cornerRadius(12)           // 圆角
    .shadow(radius: 4)          // 阴影
⚠️
修饰符顺序影响结果

.padding().background(.blue).background(.blue).padding() 结果不同:前者蓝色背景包含 padding,后者 padding 在蓝色背景外侧。

常用修饰符一览

类别修饰符说明
布局.frame(width:height:)设置尺寸
.padding()内边距
.offset(x:y:)偏移位置
.position(x:y:)绝对定位
外观.background()背景
.foregroundColor()前景色(文字/图标)
.opacity()透明度 0.0~1.0
文字.font()字体
.multilineTextAlignment()多行对齐方式
交互.onTapGesture {}点击手势
.disabled()禁用交互

2.5 组合视图

SwiftUI 鼓励将大视图拆分成小的、可复用的子视图。提取子视图不仅提升可读性,还能加速编译。

// ❌ 臃肿的单一视图
struct ContentView: View {
    var body: some View {
        VStack {
            HStack {
                Image(systemName: "person")
                VStack(alignment: .leading) {
                    Text("小明").font(.headline)
                    Text("iOS 开发者").font(.caption)
                }
            }
            // ...更多内容
        }
    }
}

// ✅ 提取为子视图
struct UserCard: View {
    let name: String
    let role: String

    var body: some View {
        HStack {
            Image(systemName: "person.circle.fill")
                .font(.system(size: 40))
                .foregroundColor(.blue)
            VStack(alignment: .leading) {
                Text(name).font(.headline)
                Text(role).font(.caption).foregroundColor(.secondary)
            }
        }
        .padding()
        .background(.regularMaterial)
        .cornerRadius(12)
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            UserCard(name: "小明", role: "iOS 开发者")
            UserCard(name: "小红", role: "设计师")
        }
    }
}

2.6 ViewBuilder 与条件视图

SwiftUI 使用 @ViewBuilder 属性包装器支持在 body 中使用条件判断和循环,这些都会被编译成视图层级。

struct StatusView: View {
    let isOnline: Bool

    var body: some View {
        HStack {
            Circle()
                .fill(isOnline ? Color.green : .gray)
                .frame(width: 10, height: 10)
            Text(isOnline ? "在线" : "离线")
                .foregroundColor(isOnline ? .green : .secondary)

            // if-else 也可以
            if isOnline {
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(.green)
            }
        }
    }
}

2.7 预览(Preview)

Xcode 提供实时预览功能,让你无需编译运行就能看到界面效果。Swift 5.9 引入了 #Preview 宏,语法更简洁,支持多个命名预览和不同外观特性。

#Preview {
    ContentView()
}

// 预览多种状态
#Preview("在线状态") {
    StatusView(isOnline: true)
}

#Preview("离线状态") {
    StatusView(isOnline: false)
}

// 指定暗色模式预览(iOS 17+)
#Preview("暗色模式", traits: .colorScheme(.dark)) {
    ContentView()
}

// 在 NavigationStack 中预览(模拟真实导航环境)
#Preview("导航中") {
    NavigationStack {
        UserCard(name: "小明", role: "iOS 开发者")
    }
}

2.8 关键名词定义

声明式 UI(Declarative UI)
一种 UI 编程范式:开发者描述"界面应该是什么样的",而非"如何一步步构建界面"。当底层数据变化时,框架自动将界面更新为新的声明状态。SwiftUI、React、Jetpack Compose 均采用此范式。与之对应的命令式 UI(UIKit、Android View)需要开发者手动管理每次更新。
修饰符(Modifier)
接受一个视图、返回一个新视图的方法,用于设置外观、布局、行为等属性。SwiftUI 修饰符采用装饰器模式——每次调用都在原有视图外包裹一层新的包装视图,多个修饰符链式调用形成视图层级树。调用顺序决定包裹顺序,因此顺序很重要。
不透明类型(Opaque Type)— some View
some View 表示"某种具体的、符合 View 协议的类型,调用者不需要知道具体类型"。每个视图的 body 返回 some View,让编译器在编译期确定具体类型,避免类型擦除的运行时开销,同时隐藏内部实现细节。
@ViewBuilder
Swift 结果构建器(Result Builder),使 body 属性及自定义容器可以包含 if/else、switch、for-in 等控制流语句来组合视图。编译器会将这些语句转换为对应的视图类型(如 _ConditionalContent),用户无需手动处理分支。
SF Symbols
Apple 提供的矢量图标库,包含超过 6000 个图标,通过 Image(systemName:) 使用。SF Symbols 图标随系统字体自动缩放,iOS 17 起支持 Animate Symbols 动效,与 SwiftUI 深度集成,是构建 Apple 原生界面的标准图标方案。
Material(材质)
iOS 15 引入的毛玻璃效果背景。.regularMaterial.thinMaterial.ultraThinMaterial 等提供不同透明度的磨砂效果,自动适配深色/浅色模式,能够模糊其后方内容,营造系统原生的层次感。

2.9 常见误区

⚠️
误区 1:在 body 中执行副作用

SwiftUI 可能多次调用 body 进行渲染,不要在其中执行网络请求、写文件等副作用。这类逻辑应放在 .task(异步)或 .onAppear 中。

// ❌ 错误:body 可能被反复调用
var body: some View {
    fetchDataFromNetwork()   // 危险!
    return Text("...")
}
// ✅ 正确:使用 .task 修饰符,视图出现时执行一次
var body: some View {
    Text(data ?? "加载中...")
        .task { data = await fetchData() }
}
⚠️
误区 2:ForEach 遗漏 id 参数

若数据模型未遵循 IdentifiableForEach 必须手动指定 id: 参数,否则 SwiftUI 无法正确追踪元素的插入/删除动画。

// ❌ 字符串数组缺少 id → 编译错误
ForEach(["苹果", "香蕉"]) { item in Text(item) }

// ✅ 指定 \.self(字符串值唯一时可用)
ForEach(["苹果", "香蕉"], id: \.self) { item in Text(item) }

// ✅ 最佳方案:遵循 Identifiable
struct Fruit: Identifiable {
    let id = UUID(); let name: String
}

2.10 实战:文章卡片列表

综合运用本章知识,构建一个完整的文章列表界面:

import SwiftUI

// 1. 数据模型(遵循 Identifiable 让 ForEach 无需手动 id)
struct Article: Identifiable {
    let id = UUID()
    let title:    String
    let summary:  String
    let category: String
}

// 2. 单张卡片(提取为独立视图,便于复用)
struct ArticleCard: View {
    let article: Article

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // 分类标签:背景色 → 圆角 → 裁剪为胶囊
            Text(article.category)
                .font(.caption.weight(.semibold))
                .padding(.horizontal, 8).padding(.vertical, 4)
                .background(.blue.opacity(0.15))
                .foregroundStyle(.blue)
                .clipShape(Capsule())

            Text(article.title)
                .font(.headline)
                .lineLimit(2)           // 最多2行,超出省略号结尾

            Text(article.summary)
                .font(.subheadline)
                .foregroundStyle(.secondary)   // 系统次要颜色,自适应暗色模式
                .lineLimit(3)
        }
        .padding()
        .background(.regularMaterial)         // 毛玻璃材质,自适应亮/暗色
        .clipShape(RoundedRectangle(cornerRadius: 12))
        .shadow(color: .black.opacity(0.08), radius: 8, y: 2)
    }
}

// 3. 列表主视图
struct ArticleListView: View {
    let articles = [
        Article(title: "SwiftUI 声明式革命",
                 summary: "探索声明式 UI 如何彻底改变 iOS 开发...",
                 category: "SwiftUI"),
        Article(title: "从 UIKit 迁移到 SwiftUI",
                 summary: "大型项目的渐进式迁移策略与实战经验分享...",
                 category: "迁移"),
    ]

    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVStack(spacing: 12) {  // LazyVStack:按需渲染,性能优于 VStack
                    ForEach(articles) { article in
                        ArticleCard(article: article)
                            .padding(.horizontal)
                    }
                }
                .padding(.top)
            }
            .navigationTitle("技术文章")
        }
    }
}

#Preview { ArticleListView() }
ℹ️
本章小结

本章全面介绍了 SwiftUI 基础架构:声明式 UI 范式、View 协议与 some View、Text/Image/Button/TextField 等核心组件、修饰符链(顺序至关重要)、子视图提取与 @ViewBuilder

三条重要记忆点:① 修饰符包裹视图,顺序影响结果;② 不要在 body 中执行副作用;③ ForEach 需要每个元素有唯一标识。下一章深入布局系统:Stack、Grid 与 ScrollView。