普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月16日首页

第4章:基础布局系统

作者 90后晨仔
2026年4月16日 21:21

Snip20260416_1.png

示例代码都放这里啦,有需要的可以下载学习。swiftUIDemo

4.1 垂直布局:VStack

VStack 介绍

VStack 是 SwiftUI 中用于垂直堆叠视图的容器,它会将子视图按垂直方向排列。VStack 是构建垂直布局的基础组件,适用于需要从上到下排列的界面元素。

基本用法

// 基本垂直栈
VStack {
    Text("第一行")
    Text("第二行")
    Text("第三行")
}

// 带间距和对齐的垂直栈
VStack(alignment: .leading, spacing: 20) {
    Text("左对齐")
    Text("第二行")
    Text("第三行")
}
.padding()

对齐方式

VStack 提供了三种主要的对齐方式:

  • .leading:左对齐
  • .center:居中对齐(默认)
  • .trailing:右对齐
  • .top.bottom:在嵌套布局中使用
// 不同对齐方式
VStack(alignment: .leading) {
    Text("左对齐")
    Text("这是一行更长的文本")
    Text("短文本")
}
.padding()

VStack(alignment: .center) {
    Text("居中对齐")
    Text("这是一行更长的文本")
    Text("短文本")
}
.padding()

VStack(alignment: .trailing) {
    Text("右对齐")
    Text("这是一行更长的文本")
    Text("短文本")
}
.padding()

嵌套 VStack

VStack 可以嵌套使用,创建更复杂的布局结构。

// 嵌套垂直栈
VStack(spacing: 10) {
    Text("标题")
        .font(.headline)
    
    VStack(alignment: .leading, spacing: 8) {
        Text("项目 1")
        Text("项目 2")
        Text("项目 3")
    }
    .padding()
    .background(Color.gray.opacity(0.1))
    .cornerRadius(8)
    
    Button("确认") {}
}
.padding()

适用场景

  • 表单布局:从上到下排列的输入字段
  • 列表项:垂直排列的内容块
  • 页面结构:标题、内容、按钮的垂直布局
  • 卡片式布局:垂直堆叠的信息卡片

性能考虑

  • VStack 会根据子视图的大小自动调整高度
  • 对于大量子视图,考虑使用 LazyVStack 来提高性能
  • 避免过深的嵌套,可能会影响渲染性能

4.2 水平布局:HStack

HStack 介绍

HStack 是 SwiftUI 中用于水平堆叠视图的容器,它会将子视图按水平方向排列。HStack 是构建水平布局的基础组件,适用于需要从左到右排列的界面元素。

基本用法

// 基本水平栈
HStack {
    Text("左侧")
    Text("中间")
    Text("右侧")
}

// 带间距和对齐的水平栈
HStack(alignment: .top, spacing: 20) {
    Text("顶部对齐")
    Text("这是一行\n多行文本")
    Text("短文本")
}
.padding()

对齐方式

HStack 提供了三种主要的对齐方式:

  • .top:顶部对齐
  • .center:居中对齐(默认)
  • .bottom:底部对齐
  • .leading.trailing:在嵌套布局中使用
// 不同对齐方式
HStack(alignment: .top) {
    Text("顶部对齐")
    Text("这是一行\n多行文本")
    Text("短文本")
}
.padding()

HStack(alignment: .center) {
    Text("居中对齐")
    Text("这是一行\n多行文本")
    Text("短文本")
}
.padding()

HStack(alignment: .bottom) {
    Text("底部对齐")
    Text("这是一行\n多行文本")
    Text("短文本")
}
.padding()

空间分配

HStack 可以使用 Spacer 来分配空间,实现更灵活的布局。

// 空间分配
HStack {
    Text("左侧")
    Spacer()  // 占据剩余空间
    Text("右侧")
}
.padding()

// 带比例的空间分配
HStack {
    Text("1/4 宽度")
        .frame(maxWidth: .infinity)
    Spacer()
    Text("1/4 宽度")
        .frame(maxWidth: .infinity)
    Spacer()
    Text("1/4 宽度")
        .frame(maxWidth: .infinity)
    Spacer()
    Text("1/4 宽度")
        .frame(maxWidth: .infinity)
}
.padding()

适用场景

  • 工具栏:水平排列的操作按钮
  • 列表项内容:左侧图标、中间文本、右侧箭头
  • 表单行:标签和输入框的水平排列
  • 导航栏:左侧返回按钮、中间标题、右侧操作按钮

性能考虑

  • HStack 会根据子视图的大小自动调整宽度
  • 对于大量子视图,考虑使用 LazyHStack 来提高性能
  • 注意水平空间不足时的布局行为,可能需要使用 ScrollView

4.3 层叠布局:ZStack

ZStack 介绍

ZStack 是 SwiftUI 中用于层叠视图的容器,它会将子视图按层叠方式排列,后面的视图会覆盖前面的视图。ZStack 是构建叠加效果的基础组件,适用于需要层级关系的界面元素。

基本用法

// 基本层叠
ZStack {
    Color.blue  // 背景
    Text("前景文本")
        .foregroundStyle(.white)
        .font(.largeTitle)
}
.frame(height: 200)

// 多层叠
ZStack {
    // 底层
    Rectangle()
        .fill(Color.yellow)
        .frame(width: 200, height: 200)
    
    // 中层
    Circle()
        .fill(Color.green)
        .frame(width: 150, height: 150)
    
    // 顶层
    Text("ZStack")
        .font(.headline)
}

对齐方式

ZStack 提供了多种对齐方式,可以精确控制子视图的位置:

  • .topLeading.top.topTrailing
  • .leading.center.trailing
  • .bottomLeading.bottom.bottomTrailing
// 不同对齐方式
ZStack(alignment: .topLeading) {
    Rectangle()
        .fill(Color.gray.opacity(0.2))
        .frame(width: 300, height: 200)
    
    Text("左上角")
        .padding(10)
}

ZStack(alignment: .center) {
    Rectangle()
        .fill(Color.gray.opacity(0.2))
        .frame(width: 300, height: 200)
    
    Text("居中")
}

ZStack(alignment: .bottomTrailing) {
    Rectangle()
        .fill(Color.gray.opacity(0.2))
        .frame(width: 300, height: 200)
    
    Text("右下角")
        .padding(10)
}

实际应用

// 带徽章的图标
ZStack(alignment: .topTrailing) {
    Image(systemName: "bell")
        .font(.system(size: 24))
    
    Circle()
        .fill(Color.red)
        .frame(width: 16, height: 16)
        .overlay {
            Text("3")
                .font(.system(size: 10))
                .foregroundStyle(.white)
        }
        .offset(x: 4, y: -4)
}

// 卡片覆盖效果
ZStack {
    RoundedRectangle(cornerRadius: 12)
        .fill(Color.white)
        .shadow(radius: 4)
        .frame(width: 300, height: 200)
    
    VStack {
        Text("卡片标题")
            .font(.headline)
        Text("卡片内容")
            .foregroundStyle(.secondary)
    }
    .padding()
    
    // 右上角标签
    Text("NEW")
        .font(.caption)
        .foregroundStyle(.white)
        .padding(4)
        .background(Color.blue)
        .cornerRadius(4)
        .offset(x: 45, y: -10)
}

适用场景

  • 带背景的文本:文本叠加在背景之上
  • 徽章效果:通知图标上的数字徽章
  • 卡片布局:带有覆盖元素的信息卡片
  • 复杂 UI 组件:需要多层叠加的自定义控件
  • 模态视图:半透明覆盖层

性能考虑

  • ZStack 会按照添加顺序渲染视图,后面的视图会覆盖前面的
  • 对于复杂的叠加效果,注意渲染性能
  • 考虑使用 offset 修饰符来微调子视图位置

4.4 间距与对齐

间距设置

间距是布局中的重要因素,它决定了视图之间的关系和视觉舒适度。

// VStack 间距
VStack(spacing: 16) {
    Text("项目 1")
    Text("项目 2")
    Text("项目 3")
}

// HStack 间距
HStack(spacing: 20) {
    Text("左")
    Text("中")
    Text("右")
}

// 嵌套栈的间距
VStack(spacing: 20) {
    Text("标题")
    
    HStack(spacing: 10) {
        Button("按钮 1") {}
        Button("按钮 2") {}
    }
    
    Text("底部文本")
}

对齐设置

对齐决定了视图在容器中的位置,影响整体布局的一致性。

// 垂直对齐
VStack(alignment: .leading) {
    Text("左对齐")
    Text("这是一行更长的文本")
}

// 水平对齐
HStack(alignment: .center) {
    Text("顶部")
        .font(.largeTitle)
    Text("底部")
        .font(.footnote)
}

// 层叠对齐
ZStack(alignment: .bottom) {
    Image(systemName: "photo")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(height: 200)
    
    Text("图片标题")
        .padding()
        .background(Color.black.opacity(0.5))
        .foregroundStyle(.white)
        .frame(maxWidth: .infinity, alignment: .center)
}

内边距与外边距

内边距(padding)和外边距是控制视图与其他元素之间空间的重要工具。

// 内边距
VStack {
    Text("带内边距的文本")
        .padding()  // 四周内边距
    
    Text("自定义内边距")
        .padding(.horizontal, 20)  // 水平内边距
        .padding(.vertical, 10)    // 垂直内边距
}

// 外边距
VStack {
    Text("带外边距的文本")
}
.padding()  // 给整个 VStack 添加内边距

// 组合使用
Text("文本")
    .padding(10)  // 内边距
    .background(Color.yellow)
    .padding(10)  // 外边距(看起来像内边距)
    .background(Color.blue)

适用场景

  • 表单设计:通过间距和对齐创建整齐的表单
  • 卡片布局:使用内边距和外边距创建视觉层次感
  • 响应式设计:根据不同屏幕尺寸调整间距
  • 可访问性:适当的间距提高内容的可读性

最佳实践

  • 保持一致的间距系统:使用统一的间距值(如 8、16、24 等)
  • 考虑内容的重要性:重要内容之间应有更大的间距
  • 响应式调整:在不同屏幕尺寸上调整间距
  • 测试不同设备:确保在各种设备上布局都美观

4.5 垫片:Spacer

Spacer 介绍

Spacer 是 SwiftUI 中用于占据剩余空间的视图,它会自动扩展以填充可用空间。Spacer 是实现灵活布局的重要工具,特别适用于需要将元素推到容器边缘的场景。

基本用法

// 水平布局中的 Spacer
HStack {
    Text("左侧")
    Spacer()  // 占据中间的所有空间
    Text("右侧")
}
.padding()

// 垂直布局中的 Spacer
VStack {
    Text("顶部")
    Spacer()  // 占据中间的所有空间
    Text("底部")
}
.frame(height: 200)
.padding()

灵活使用

// 顶部对齐
VStack {
    Text("标题")
    Spacer()
}
.frame(height: 200)
.padding()

// 底部对齐
VStack {
    Spacer()
    Text("底部文本")
}
.frame(height: 200)
.padding()

// 两端对齐
HStack {
    Text("左侧")
    Spacer()
    Text("中间")
    Spacer()
    Text("右侧")
}
.padding()

实际应用

// 工具栏布局
HStack {
    Button("返回") {
        print("返回")
    }
    
    Spacer()
    
    Text("页面标题")
    
    Spacer()
    
    Button("更多") {
        print("更多")
    }
}
.padding()
.background(Color(.systemBackground))

// 表单底部按钮
VStack {
    // 表单内容
    ForEach(0..<3) {
        Text("表单项 \($0 + 1)")
            .padding()
            .background(Color.gray.opacity(0.1))
            .cornerRadius(8)
            .padding(.horizontal)
    }
    
    Spacer()
    
    // 底部按钮
    Button("提交") {
        print("提交")
    }
    .buttonStyle(.borderedProminent)
    .padding()
}

适用场景

  • 工具栏:将标题居中,按钮放在两侧
  • 表单:将提交按钮固定在底部
  • 卡片:将内容推到顶部,操作按钮放在底部
  • 导航栏:创建平衡的布局

性能考虑

  • Spacer 是轻量级视图,对性能影响很小
  • 合理使用 Spacer 可以减少不必要的几何计算
  • 避免在不需要的地方使用 Spacer,可能会导致意外的布局行为

4.6 布局修饰符

框架修饰符

frame 修饰符用于控制视图的大小和对齐方式。

// 设置固定大小
Text("固定大小")
    .frame(width: 200, height: 100)
    .background(Color.yellow)

// 设置最大和最小大小
Text("灵活大小")
    .frame(minWidth: 100, maxWidth: 300, minHeight: 50, maxHeight: 150)
    .background(Color.blue)

// 填充父容器
Text("填充")
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(Color.green)

// 带对齐的框架
Text("右对齐")
    .frame(width: 200, alignment: .trailing)
    .background(Color.gray.opacity(0.2))

位置修饰符

positionoffset 修饰符用于调整视图的位置。

// 绝对位置
Text("绝对位置")
    .position(x: 100, y: 100)

// 相对偏移
Text("相对偏移")
    .offset(x: 50, y: 20)

// 组合使用
ZStack {
    Text("基础位置")
        .background(Color.yellow)
    
    Text("偏移位置")
        .offset(x: 50, y: 30)
        .background(Color.red)
}

布局优先级

layoutPriority 修饰符用于设置视图的布局优先级。

HStack {
    Text("短文本")
        .layoutPriority(1)  // 高优先级
        .background(Color.yellow)
    
    Text("这是一段非常长的文本,会被截断")
        .background(Color.blue)
}
.frame(width: 200)

适用场景

  • 响应式设计:根据屏幕尺寸调整视图大小
  • 自定义布局:精确控制视图位置
  • 复杂界面:处理不同优先级的内容
  • 动态布局:根据内容自动调整

4.7 容器布局

List

List 是用于显示滚动列表的容器,自动处理单元格布局。

// 基本列表
List {
    Text("项目 1")
    Text("项目 2")
    Text("项目 3")
}

// 带分组的列表
List {
    Section(header: Text("分组 1")) {
        Text("项目 1")
        Text("项目 2")
    }
    
    Section(header: Text("分组 2")) {
        Text("项目 3")
        Text("项目 4")
    }
}

ScrollView

ScrollView 用于创建可滚动的内容区域。

// 垂直滚动
ScrollView {
    VStack(spacing: 20) {
        ForEach(0..<20) {
            Text("项目 \($0 + 1)")
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(8)
        }
    }
    .padding()
}

// 水平滚动
ScrollView(.horizontal) {
    HStack(spacing: 20) {
        ForEach(0..<10) {
            Text("项目 \($0 + 1)")
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(8)
        }
    }
    .padding()
}

LazyVStack 和 LazyHStack

LazyVStackLazyHStack 是延迟加载的栈容器,适用于大量数据。

// 延迟加载的垂直栈
ScrollView {
    LazyVStack(spacing: 20) {
        ForEach(0..<1000) {
            Text("项目 \($0 + 1)")
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(8)
        }
    }
    .padding()
}

适用场景

  • List:显示结构化数据列表
  • ScrollView:显示超出屏幕的内容
  • LazyVStack/LazyHStack:处理大量数据,提高性能

实战:创建一个登录页面

需求分析

创建一个包含以下元素的登录页面:

  1. 应用图标和标题
  2. 用户名输入框
  3. 密码输入框(带可见性切换)
  4. 登录按钮
  5. 忘记密码链接
  6. 注册链接

代码实现

import SwiftUI

struct LoginView: View {
    // 状态变量
    @State private var username = ""
    @State private var password = ""
    @State private var showPassword = false
    
    var body: some View {
        ZStack {
            // 背景
            LinearGradient(
                colors: [.blue.opacity(0.1), .purple.opacity(0.1)],
                startPoint: .top,
                endPoint: .bottom
            )
            .ignoresSafeArea()
            
            VStack(spacing: 24) {
                // 应用图标和标题
                VStack(spacing: 12) {
                    Image(systemName: "lock.fill")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 80, height: 80)
                        .foregroundStyle(.blue)
                    
                    Text("欢迎回来")
                        .font(.largeTitle)
                        .fontWeight(.bold)
                    
                    Text("请登录以继续")
                        .foregroundStyle(.secondary)
                }
                
                // 输入区域
                VStack(spacing: 16) {
                    // 用户名输入框
                    TextField(
                        "用户名",
                        text: $username,
                        prompt: Text("请输入用户名")
                    )
                    .textFieldStyle(.roundedBorder)
                    .padding(.horizontal)
                    
                    // 密码输入框
                    ZStack(alignment: .trailing) {
                        if showPassword {
                            TextField(
                                "密码",
                                text: $password,
                                prompt: Text("请输入密码")
                            )
                        } else {
                            SecureField(
                                "密码",
                                text: $password,
                                prompt: Text("请输入密码")
                            )
                        }
                        
                        Button(action: {
                            showPassword.toggle()
                        }) {
                            Image(systemName: showPassword ? "eye.slash.fill" : "eye.fill")
                                .foregroundStyle(.secondary)
                                .padding(.trailing, 16)
                        }
                    }
                    .textFieldStyle(.roundedBorder)
                    .padding(.horizontal)
                    
                    // 忘记密码
                    HStack {
                        Spacer()
                        Button("忘记密码?") {
                            print("忘记密码")
                        }
                        .foregroundStyle(.blue)
                        .padding(.trailing)
                    }
                }
                
                // 登录按钮
                Button("登录") {
                    print("登录")
                }
                .buttonStyle(.borderedProminent)
                .tint(.blue)
                .padding(.horizontal)
                .frame(maxWidth: .infinity)
                
                // 注册链接
                HStack {
                    Text("还没有账号?")
                    Button("立即注册") {
                        print("注册")
                    }
                    .foregroundStyle(.blue)
                }
                
                Spacer()
            }
            .padding(.top, 60)
        }
    }
}

#Preview {
    LoginView()
}

代码解析

  1. ZStack:用于层叠背景和内容,创建深度感
  2. VStack:用于垂直排列各个部分,保持页面结构清晰
  3. HStack:用于水平排列忘记密码链接和注册链接
  4. Spacer:用于底部填充空间,将内容推到顶部
  5. TextField 和 SecureField:用于用户输入
  6. Button:用于操作按钮
  7. LinearGradient:用于创建美观的背景渐变
  8. @State:用于管理视图状态

实战:创建一个产品详情页

需求分析

创建一个产品详情页,包含以下元素:

  1. 产品图片
  2. 产品标题和价格
  3. 产品描述
  4. 规格选择
  5. 购买按钮

代码实现

import SwiftUI

struct ProductDetailView: View {
    // 状态变量
    @State private var selectedColor = "红色"
    @State private var selectedSize = "M"
    @State private var quantity = 1
    
    // 产品数据
    let productName = "SwiftUI 高级教程"
    let productPrice = "¥99.00"
    let productDescription = "本教程涵盖了 SwiftUI 的高级特性,包括动画、手势、布局和性能优化等内容。通过实际项目案例,帮助你掌握 SwiftUI 的核心概念和最佳实践。"
    let colors = ["红色", "蓝色", "黑色"]
    let sizes = ["S", "M", "L", "XL"]
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                // 产品图片
                ZStack {
                    Color.gray.opacity(0.1)
                        .frame(height: 300)
                    
                    Image(systemName: "book.fill")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 150, height: 150)
                        .foregroundStyle(.blue)
                }
                
                // 产品信息
                VStack(alignment: .leading, spacing: 12) {
                    HStack {
                        Text(productName)
                            .font(.title)
                            .fontWeight(.bold)
                        Spacer()
                        Text(productPrice)
                            .font(.title)
                            .fontWeight(.bold)
                            .foregroundStyle(.red)
                    }
                    
                    // 产品描述
                    Text(productDescription)
                        .foregroundStyle(.secondary)
                        .lineLimit(nil)
                    
                    // 颜色选择
                    Text("颜色")
                        .font(.headline)
                    HStack(spacing: 10) {
                        ForEach(colors, id: \.self) {
                            color in
                            Button(action: {
                                selectedColor = color
                            }) {
                                Text(color)
                                    .padding(8)
                                    .background(selectedColor == color ? Color.blue : Color.gray.opacity(0.1))
                                    .foregroundStyle(selectedColor == color ? .white : .primary)
                                    .cornerRadius(4)
                            }
                        }
                    }
                    
                    // 尺寸选择
                    Text("尺寸")
                        .font(.headline)
                    HStack(spacing: 10) {
                        ForEach(sizes, id: \.self) {
                            size in
                            Button(action: {
                                selectedSize = size
                            }) {
                                Text(size)
                                    .padding(8)
                                    .background(selectedSize == size ? Color.blue : Color.gray.opacity(0.1))
                                    .foregroundStyle(selectedSize == size ? .white : .primary)
                                    .cornerRadius(4)
                            }
                        }
                    }
                    
                    // 数量选择
                    Text("数量")
                        .font(.headline)
                    HStack {
                        Button(action: {
                            if quantity > 1 {
                                quantity -= 1
                            }
                        }) {
                            Image(systemName: "minus.circle")
                                .font(.system(size: 24))
                        }
                        
                        Text("\(quantity)")
                            .font(.headline)
                            .padding(.horizontal, 20)
                        
                        Button(action: {
                            quantity += 1
                        }) {
                            Image(systemName: "plus.circle")
                                .font(.system(size: 24))
                        }
                    }
                }
                .padding()
                
                // 购买按钮
                Button("加入购物车") {
                    print("加入购物车")
                }
                .buttonStyle(.borderedProminent)
                .tint(.blue)
                .padding(.horizontal)
                .frame(maxWidth: .infinity)
                .padding(.bottom, 30)
            }
        }
        .navigationTitle("产品详情")
        .navigationBarTitleDisplayMode(.inline)
    }
}

#Preview {
    ProductDetailView()
}

代码解析

  1. ScrollView:用于滚动显示产品详情
  2. ZStack:用于显示产品图片和背景
  3. VStack:用于垂直排列产品信息
  4. HStack:用于水平排列价格、颜色选择、尺寸选择和数量控制
  5. Button:用于选择颜色、尺寸和调整数量
  6. @State:用于管理用户选择的状态

小结

本章详细介绍了 SwiftUI 中的基础布局系统,包括:

  • VStack:垂直堆叠视图,适用于从上到下的布局
  • HStack:水平堆叠视图,适用于从左到右的布局
  • ZStack:层叠视图,适用于需要层级关系的布局
  • 间距与对齐:控制视图之间的空间和位置关系
  • Spacer:占据剩余空间,实现灵活布局
  • 布局修饰符:控制视图的大小、位置和优先级
  • 容器布局:List、ScrollView、LazyVStack 等高级容器
  • 实战案例:登录页面和产品详情页的完整实现

布局最佳实践

  1. 保持简洁:使用最少的容器实现所需布局
  2. 嵌套合理:避免过深的布局嵌套
  3. 响应式设计:考虑不同屏幕尺寸的布局适配
  4. 性能优化:对于大量数据使用 Lazy 容器
  5. 一致性:保持间距和对齐的一致性
  6. 可访问性:确保布局对所有用户都友好

通过本章的学习,你已经掌握了 SwiftUI 中最基本的布局技巧,能够创建各种常见的布局结构。在实际开发中,你可以根据具体需求选择合适的布局容器和技术,创建美观、响应式的用户界面。


参考资料


本内容为《SwiftUI 基础教程》第四章,欢迎关注后续更新。

第3章:基础视图组件

作者 90后晨仔
2026年4月16日 20:46

Snip20260416_1.png

示例代码都放这里啦,有需要的可以下载学习。swiftUIDemo

3.1 文本显示:Text

Text 组件介绍

Text 是 SwiftUI 中最基本的视图组件,用于显示文本内容。它支持富文本、字体样式、颜色等多种属性。

基本用法

// 基本文本
Text("Hello, SwiftUI!")

// 带样式的文本
Text("Hello, SwiftUI!")
    .font(.largeTitle)         // 设置字体大小
    .fontWeight(.bold)         // 设置字重
    .foregroundStyle(.blue)    // 设置文本颜色
    .italic()                  // 斜体
    .underline()               // 下划线
    .strikethrough()           // 删除线

富文本

// 富文本
Text("Hello, \(Text("SwiftUI").foregroundStyle(.blue).bold())!")

// 多行文本
Text("这是一段多行文本,\n可以通过反斜杠 n 来换行,\n或者直接在字符串中换行。")
    .multilineTextAlignment(.center)  // 多行文本对齐方式
    .lineLimit(3)                     // 限制行数
    .truncationMode(.tail)            // 截断方式

本地化

// 本地化文本
Text("welcome_message")  // 从 Localizable.strings 文件中读取

// 带参数的本地化
Text("greeting", comment: "欢迎语")

// 格式化文本
let name = "张三"
Text("欢迎 %@", name)

日期和数字格式化

// 日期格式化
let date = Date()
Text(date, style: .date)           // 仅日期
Text(date, style: .time)           // 仅时间
Text(date, style: .relative)       // 相对时间
Text(date, style: .offset)         // 时间偏移
Text(date, style: .timer)          // 计时器

// 数字格式化
let number = 123456.789
Text(number, format: .number)
Text(number, format: .currency(code: "CNY"))
Text(number, format: .percent)

3.2 图片显示:Image

Image 组件介绍

Image 用于显示图片,可以从系统图标、资源文件或网络加载图片。

基本用法

// 系统图标
Image(systemName: "star.fill")

// 资源文件图片
Image("avatar")

// 网络图片 (iOS 15+)
AsyncImage(url: URL(string: "https://example.com/image.jpg")) { phase in
    switch phase {
    case .empty:
        ProgressView()  // 加载中
    case .success(let image):
        image
            .resizable()
            .aspectRatio(contentMode: .fill)
    case .failure:
        Image(systemName: "photo")  // 加载失败
    @unknown default:
        EmptyView()
    }
}

图片修饰符

Image("avatar")
    .resizable()                 // 可调整大小
    .aspectRatio(contentMode: .fit)  // 内容模式
    .frame(width: 100, height: 100)  // 设置大小
    .clipShape(Circle())         // 裁剪形状
    .overlay(                    // 叠加内容
        Circle()
            .stroke(Color.blue, lineWidth: 2)
    )
    .shadow(radius: 5)           // 阴影
    .opacity(0.8)                // 透明度

系统图标

// 系统图标
Image(systemName: "heart.fill")
    .foregroundStyle(.red)
    .font(.system(size: 24))

// 多色图标 (iOS 15+)
Image(systemName: "person.fill.badge.plus")
    .symbolRenderingMode(.multicolor)

// 可变颜色图标
Image(systemName: "star")
    .foregroundStyle(.yellow)

3.3 按钮交互:Button

Button 组件介绍

Button 用于创建可点击的按钮,支持多种样式和交互方式。

基本用法

// 基本按钮
Button("点击我") {
    print("按钮被点击了")
}

// 带图标的按钮
Button {
    print("按钮被点击了")
} label: {
    HStack {
        Image(systemName: "star.fill")
        Text("喜欢")
    }
}

// 带角色的按钮
Button("删除", role: .destructive) {
    print("删除操作")
}

按钮样式

// 边框按钮
Button("边框按钮") {
    // 操作
}
.buttonStyle(.bordered)

// 突出显示的按钮
Button("突出按钮") {
    // 操作
}
.buttonStyle(.borderedProminent)
.tint(.blue)  // 按钮颜色

// 胶囊按钮
Button("胶囊按钮") {
    // 操作
}
.buttonStyle(.borderedProminent)
.tint(.green)
.cornerRadius(20)

// 文本按钮
Button("文本按钮") {
    // 操作
}
.buttonStyle(.plain)

禁用状态

@State private var isEnabled = false

Button("禁用按钮") {
    // 操作
}
.disabled(!isEnabled)
.opacity(isEnabled ? 1.0 : 0.5)

3.4 输入控件:TextField、SecureField、TextEditor

TextField 文本输入框

@State private var text = ""

TextField("请输入文本", text: $text)
    .textFieldStyle(.roundedBorder)  // 边框样式
    .padding()                      // 内边距
    .keyboardType(.default)         // 键盘类型
    .autocapitalization(.sentences) // 自动大写
    .autocorrectionDisabled(true)   // 禁用自动纠正

// 带提示的 TextField
TextField(
    "请输入用户名",
    text: $text,
    prompt: Text("用户名不能为空")
        .foregroundStyle(.secondary)
)
.textFieldStyle(.roundedBorder)

SecureField 安全输入框

@State private var password = ""

SecureField("请输入密码", text: $password)
    .textFieldStyle(.roundedBorder)
    .padding()

// 带可见性切换的密码输入
@State private var showPassword = false

ZStack(alignment: .trailing) {
    if showPassword {
        TextField("请输入密码", text: $password)
    } else {
        SecureField("请输入密码", text: $password)
    }
    Button(action: {
        showPassword.toggle()
    }) {
        Image(systemName: showPassword ? "eye.slash.fill" : "eye.fill")
            .foregroundStyle(.secondary)
            .padding(.trailing, 8)
    }
}
.textFieldStyle(.roundedBorder)
.padding()

TextEditor 多行文本编辑器

@State private var message = ""

TextEditor(text: $message)
    .frame(height: 150)           // 设置高度
    .border(Color.gray.opacity(0.3), width: 1)  // 边框
    .cornerRadius(8)              // 圆角
    .padding()
    .foregroundStyle(.primary)    // 文本颜色

// 带占位符的 TextEditor
ZStack(alignment: .topLeading) {
    TextEditor(text: $message)
        .frame(height: 150)
        .padding(8)
    
    if message.isEmpty {
        Text("请输入消息...")
            .foregroundStyle(.secondary)
            .padding(10)
            .allowsHitTesting(false)  // 允许点击穿透
    }
}
.border(Color.gray.opacity(0.3), width: 1)
.cornerRadius(8)
.padding()

3.5 开关与选择:Toggle、Picker、Slider、Stepper

Toggle 开关

@State private var isEnabled = false

Toggle("启用功能", isOn: $isEnabled)
    .toggleStyle(.switch)  // 开关样式
    .padding()

// 带图标的 Toggle
Toggle(isOn: $isEnabled) {
    HStack {
        Image(systemName: "bell.fill")
        Text("接收通知")
    }
}
.toggleStyle(.switch)
.padding()

Picker 选择器

@State private var selectedOption = "选项1"
let options = ["选项1", "选项2", "选项3"]

// 分段控件样式
Picker("选择", selection: $selectedOption) {
    ForEach(options, id: \.self) {
        Text($0)
    }
}
.pickerStyle(.segmented)
.padding()

// 菜单样式
Picker("选择", selection: $selectedOption) {
    ForEach(options, id: \.self) {
        Text($0)
    }
}
.pickerStyle(.menu)
.padding()

// 轮盘样式(iOS 14+)
@State private var selectedColor = Color.red
let colors: [Color] = [.red, .green, .blue, .yellow]

Picker("颜色", selection: $selectedColor) {
    ForEach(colors, id: \.self) {
        ColorPickerView(color: $0)
    }
}
.pickerStyle(.wheel)
.frame(height: 200)
.padding()

// 辅助视图
struct ColorPickerView: View {
    let color: Color
    var body: some View {
        HStack {
            Rectangle()
                .fill(color)
                .frame(width: 20, height: 20)
                .cornerRadius(4)
            Text(String(describing: color))
        }
    }
}

Slider 滑块

@State private var value = 0.5

Slider(value: $value, in: 0...1)
    .padding()
    .tint(.blue)  // 滑块颜色

// 带标签的滑块
Slider(
    value: $value,
    in: 0...1,
    label: { Text("亮度") },
    minimumValueLabel: { Text("暗") },
    maximumValueLabel: { Text("亮") }
)
.padding()

// 整数滑块
@State private var intValue = 5

Slider(value: Binding(
    get: { Double(intValue) },
    set: { intValue = Int($0) }
), in: 0...10, step: 1)
.padding()
Text("值:\(intValue)")

Stepper 步进器

@State private var count = 0

Stepper("数量:\(count)", value: $count)
    .padding()

// 带范围的步进器
Stepper(
    "数量:\(count)",
    value: $count,
    in: 0...10,
    step: 2
)
.padding()

// 带标签的步进器
Stepper {
    Text("数量:\(count)")
} onIncrement: {
    count += 1
    print("增加到:\(count)")
} onDecrement: {
    count -= 1
    print("减少到:\(count)")
}
.padding()

3.6 进度指示:ProgressView

不确定进度

// 基本进度指示器
ProgressView()

// 带标签的进度指示器
ProgressView("加载中...")

// 带样式的进度指示器
ProgressView("处理中...")
    .progressViewStyle(.circular)
    .tint(.blue)
    .padding()

确定进度

@State private var progress = 0.0

ProgressView("下载进度", value: progress, total: 1.0)
    .padding()

// 带百分比的进度条
ProgressView(
    value: progress,
    total: 1.0,
    label: { Text("下载进度") },
    currentValueLabel: { Text("\(Int(progress * 100))%") }
)
.padding()

// 水平进度条样式
ProgressView(value: progress, total: 1.0)
    .progressViewStyle(.linear)
    .tint(.green)
    .frame(height: 10)
    .padding()

实战:创建一个用户设置页面

需求分析

创建一个包含以下元素的用户设置页面:

  1. 个人信息区域
  2. 通知设置(开关)
  3. 主题选择(选择器)
  4. 字体大小(滑块)
  5. 清除缓存按钮
  6. 退出登录按钮

代码实现

import SwiftUI

struct SettingsView: View {
    // 状态变量
    @State private var notificationsEnabled = true
    @State private var selectedTheme = "浅色"
    @State private var fontSize = 16.0
    @State private var cacheSize = "128 MB"
    
    // 主题选项
    let themes = ["浅色", "深色", "跟随系统"]
    
    var body: some View {
        NavigationStack {
            List {
                // 个人信息区域
                Section {
                    HStack {
                        Image(systemName: "person.circle.fill")
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(width: 60, height: 60)
                            .foregroundStyle(.blue)
                        
                        VStack(alignment: .leading, spacing: 4) {
                            Text("张三")
                                .font(.headline)
                            Text("zhangsan@example.com")
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                        }
                        Spacer()
                        Image(systemName: "chevron.right")
                            .foregroundStyle(.secondary)
                    }
                    .padding(.vertical, 8)
                }
                
                // 通知设置
                Section("通知设置") {
                    Toggle("接收推送通知", isOn: $notificationsEnabled)
                    Toggle("声音提醒", isOn: $notificationsEnabled)
                    Toggle("振动提醒", isOn: $notificationsEnabled)
                }
                
                // 外观设置
                Section("外观设置") {
                    Picker("主题", selection: $selectedTheme) {
                        ForEach(themes, id: \.self) {
                            Text($0)
                        }
                    }
                    .pickerStyle(.menu)
                    
                    VStack(alignment: .leading, spacing: 8) {
                        Text("字体大小:\(Int(fontSize))")
                        Slider(value: $fontSize, in: 12...24, step: 1)
                            .tint(.blue)
                    }
                }
                
                // 存储设置
                Section("存储设置") {
                    HStack {
                        Text("缓存大小")
                        Spacer()
                        Text(cacheSize)
                            .foregroundStyle(.secondary)
                    }
                    Button("清除缓存") {
                        // 清除缓存逻辑
                        print("清除缓存")
                    }
                    .foregroundStyle(.blue)
                }
                
                // 账户设置
                Section {
                    Button("关于我们") {
                        // 关于我们逻辑
                    }
                    Button("隐私政策") {
                        // 隐私政策逻辑
                    }
                    Button("退出登录", role: .destructive) {
                        // 退出登录逻辑
                    }
                }
            }
            .navigationTitle("设置")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

#Preview {
    SettingsView()
}

代码解析

  • List 和 Section:使用列表和分组组织设置项
  • NavigationStack:提供导航功能
  • Toggle:用于开关设置
  • Picker:用于主题选择
  • Slider:用于调整字体大小
  • Button:用于操作按钮
  • HStack 和 VStack:用于布局
  • @State:用于管理视图状态

小结

本章介绍了 SwiftUI 中的基础视图组件,包括:

  • Text:文本显示,支持富文本、本地化和格式化
  • Image:图片显示,支持系统图标、资源文件和网络图片
  • Button:按钮交互,支持多种样式和角色
  • 输入控件:TextFieldSecureFieldTextEditor
  • 选择控件:TogglePickerSliderStepper
  • 进度指示:ProgressView
  • 一个完整的用户设置页面实战

通过本章的学习,你已经掌握了 SwiftUI 中最常用的基础组件,能够创建各种常见的用户界面元素。


参考资料


本内容为《SwiftUI 基础教程》第三章,欢迎关注后续更新。

昨天 — 2026年4月15日首页

第2章:声明式 UI 基础

作者 90后晨仔
2026年4月15日 22:22

2.1 声明式 vs 命令式 UI 对比

命令式 UI(UIKit)

命令式编程是一种传统的编程范式,开发者需要明确告诉计算机“如何”完成任务。在 UIKit 中,你需要:

  1. 创建视图对象
  2. 配置视图属性
  3. 添加视图到视图层级
  4. 设置布局约束
  5. 手动更新视图状态

示例代码(UIKit)

import UIKit

class ProfileViewController: UIViewController {
    private let nameLabel = UILabel()
    private let avatarImageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. 创建视图
        view.backgroundColor = .white
        
        // 2. 配置 nameLabel
        nameLabel.text = "张三"
        nameLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium)
        nameLabel.textColor = .black
        nameLabel.textAlignment = .center
        
        // 3. 配置 avatarImageView
        avatarImageView.image = UIImage(named: "avatar")
        avatarImageView.contentMode = .scaleAspectFill
        avatarImageView.layer.cornerRadius = 40
        avatarImageView.clipsToBounds = true
        
        // 4. 添加到视图层级
        view.addSubview(avatarImageView)
        view.addSubview(nameLabel)
        
        // 5. 设置约束
        avatarImageView.translatesAutoresizingMaskIntoConstraints = false
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            avatarImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            avatarImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
            avatarImageView.widthAnchor.constraint(equalToConstant: 80),
            avatarImageView.heightAnchor.constraint(equalToConstant: 80),
            
            nameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 20),
            nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }
    
    // 6. 更新数据时需要手动刷新 UI
    func updateProfile(name: String, avatar: String) {
        nameLabel.text = name
        avatarImageView.image = UIImage(named: avatar)
    }
}

声明式 UI(SwiftUI)

声明式编程是一种现代的编程范式,开发者只需要描述“是什么”,而不需要关心“如何”实现。在 SwiftUI 中,你只需要:

  1. 描述界面的结构
  2. 绑定状态
  3. 系统自动处理更新

示例代码(SwiftUI)

import SwiftUI

struct ProfileView: View {
    let name: String
    let avatar: String
    
    var body: some View {
        VStack(spacing: 20) {
            // 1. 头像
            Image(avatar)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 80, height: 80)
                .clipShape(Circle())
            
            // 2. 姓名
            Text(name)
                .font(.system(size: 18, weight: .medium))
                .foregroundStyle(.primary)
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color(.systemBackground))
    }
}

// 使用示例
ProfileView(name: "张三", avatar: "avatar")

核心差异

维度 命令式 UI(UIKit) 声明式 UI(SwiftUI)
代码量 多(需要手动管理所有细节) 少(只描述结果)
状态同步 手动更新 UI 自动同步状态
可读性 较低(逻辑分散) 高(逻辑集中)
维护性 较低(容易遗漏更新) 高(状态驱动自动更新)
错误率 较高(手动操作容易出错) 较低(框架保证一致性)
开发效率 低(重复代码多) 高(简洁明了)

2.2 View 协议与 body 计算属性

View 协议

在 SwiftUI 中,所有的视图都必须遵循 View 协议:

public protocol View {
    associatedtype Body : View
    @ViewBuilder var body: Self.Body { get }
}

核心概念

  • associatedtype Body:关联类型,表示视图的内容类型
  • body:计算属性,返回视图的内容
  • @ViewBuilder:属性包装器,允许使用声明式语法组合多个视图

body 计算属性

body 是 SwiftUI 视图的核心,它是一个计算属性,每次状态变化时都会重新计算。

重要特性

  1. 计算属性:不是存储属性,每次访问都会重新计算
  2. 轻量级:应该保持简洁,避免复杂计算
  3. 返回类型:必须返回一个遵循 View 协议的类型
  4. 自动合成:@ViewBuilder 允许使用简洁的语法组合多个视图

示例

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, SwiftUI!")
            Button("Tap Me") {}
        }
    }
}

理解 some View

some View 是一个不透明类型(Opaque Type),它表示:

  • 这个函数返回一个遵循 View 协议的类型
  • 但具体是什么类型,不需要暴露给调用者
  • 编译器可以进行更多的优化

2.3 结构体视图与值类型

视图是结构体

在 SwiftUI 中,视图是使用结构体(struct)实现的,这与 UIKit 中的类(class)不同。

结构体的优势

  1. 值类型:传递时会复制,避免引用计数问题
  2. 轻量级:分配在栈内存上,创建和销毁成本低
  3. 不可变:默认不可变,状态通过 @State 等包装器管理
  4. 线程安全:值类型天生线程安全

结构体的生命周期

SwiftUI 视图的生命周期与结构体的实例化无关:

  • 结构体可能被频繁创建和销毁
  • 但底层的真实视图对象由 SwiftUI 管理
  • 视图的身份(Identity)由其在视图树中的位置决定

示例

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

count 改变时:

  1. SwiftUI 检测到状态变化
  2. 重新创建 CounterView 结构体实例
  3. 调用 body 计算属性生成新的视图树
  4. 与旧视图树对比,只更新变化的部分

2.4 修饰符(Modifier)的使用

什么是修饰符?

修饰符是 SwiftUI 中用于修改视图属性和行为的方法。它们通常以点语法链式调用。

修饰符的工作原理

修饰符不是直接修改原视图,而是返回一个新的视图,这个新视图包含了原视图和应用的修改。

示例

Text("Hello")
    .font(.largeTitle)        // 返回一个新的 Text 视图,字体为 largeTitle
    .foregroundStyle(.blue)    // 返回一个新的视图,文本颜色为蓝色
    .padding()                // 返回一个新的视图,带有内边距

常用修饰符

布局修饰符

  • padding():添加内边距
  • frame():设置视图大小和对齐方式
  • background():设置背景
  • foregroundStyle():设置前景样式(颜色、渐变等)
  • clipShape():裁剪视图形状
  • overlay():在视图上叠加内容

排版修饰符

  • font():设置字体
  • bold():加粗文本
  • italic():斜体文本
  • multilineTextAlignment():多行文本对齐
  • lineLimit():限制文本行数

交互修饰符

  • onTapGesture():添加点击手势
  • disabled():禁用视图
  • accessibility():添加无障碍支持

动画修饰符

  • animation():添加动画
  • transition():添加转场动画

修饰符的顺序

修饰符的顺序很重要,因为每个修饰符都会作用于前一个修饰符返回的视图。

示例

// 先设置背景,再添加内边距
Text("Hello")
    .background(Color.blue)
    .padding()

// 先添加内边距,再设置背景
Text("Hello")
    .padding()
    .background(Color.blue)

这两种写法会产生不同的效果,第一种背景只覆盖文本区域,第二种背景会覆盖整个内边距区域。

实战:创建一个信息卡片

需求分析

创建一个包含以下元素的信息卡片:

  1. 标题
  2. 副标题
  3. 描述文本
  4. 图标
  5. 卡片样式(圆角、阴影)

代码实现

import SwiftUI

struct InfoCardView: View {
    // 卡片数据
    let title: String
    let subtitle: String
    let description: String
    let iconName: String
    let iconColor: Color
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // 图标和标题区域
            HStack(alignment: .center, spacing: 12) {
                // 图标
                Circle()
                    .fill(iconColor.opacity(0.1))
                    .frame(width: 48, height: 48)
                    .overlay {
                        Image(systemName: iconName)
                            .resizable()
                            .scaledToFit()
                            .frame(width: 24, height: 24)
                            .foregroundStyle(iconColor)
                    }
                
                // 标题和副标题
                VStack(alignment: .leading, spacing: 4) {
                    Text(title)
                        .font(.headline)
                        .fontWeight(.semibold)
                    Text(subtitle)
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
            }
            
            // 描述文本
            Text(description)
                .font(.body)
                .foregroundStyle(.primary)
                .lineLimit(nil) // 不限制行数
        }
        .padding(16) // 卡片内边距
        .background(
            RoundedRectangle(cornerRadius: 12)
                .fill(Color(.systemBackground))
                .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
        )
        .padding(.horizontal, 16) // 卡片外边距
    }
}

// 使用示例
struct ContentView: View {
    var body: some View {
        VStack(spacing: 16) {
            InfoCardView(
                title: "SwiftUI 简介",
                subtitle: "现代 UI 框架",
                description: "SwiftUI 是一个声明式 UI 框架,允许开发者使用 Swift 语言创建跨 Apple 平台的用户界面。它提供了简洁、直观的语法,使 UI 开发变得更加高效。",
                iconName: "star.fill",
                iconColor: .yellow
            )
            
            InfoCardView(
                title: "声明式编程",
                subtitle: "现代编程范式",
                description: "声明式编程让开发者只需要描述界面的样子,而不需要关心如何实现。系统会自动处理视图的创建、更新和销毁。",
                iconName: "code",
                iconColor: .blue
            )
            
            InfoCardView(
                title: "跨平台",
                subtitle: "一次编写,多处运行",
                description: "SwiftUI 支持 iOS、iPadOS、macOS、watchOS 和 tvOS,让你的代码可以在所有 Apple 平台上运行。",
                iconName: "globe",
                iconColor: .green
            )
        }
        .padding(.vertical, 16)
        .background(Color(.systemGroupedBackground))
    }
}

#Preview {
    ContentView()
}

代码解析

  1. 结构体参数:通过参数传递卡片数据,使视图可复用
  2. VStack 和 HStack:使用栈布局组织视图
  3. Circle:创建圆形背景
  4. overlay:在圆形上叠加图标
  5. RoundedRectangle:创建圆角矩形背景
  6. shadow:添加阴影效果
  7. spacing:设置栈视图的间距
  8. alignment:设置栈视图的对齐方式

小结

本章介绍了声明式 UI 的基础概念,包括:

  • 声明式 vs 命令式 UI 的对比
  • View 协议与 body 计算属性
  • 结构体视图与值类型的特性
  • 修饰符的使用方法和顺序
  • 一个信息卡片的实战实现

通过本章的学习,你已经了解了 SwiftUI 的基本工作原理和核心概念,为后续的学习打下了坚实的基础。

参考资料

第1章:SwiftUI 与开发环境简介

作者 90后晨仔
2026年4月15日 21:55

距离上一次学习SwiftUI已经过去几年的时间了,好多知识点都些忘记了,最近刚好有有一些时间就好好的在从零回顾一下吧。


1.1 什么是 SwiftUI?

官方定义

根据 Apple 官方文档,SwiftUI 是一个现代的声明式 UI 框架,它允许开发者使用 Swift 语言创建跨 Apple 平台的用户界面。

SwiftUI 的核心优势

  1. 声明式语法:描述界面“是什么”而不是“怎么做”
  2. 跨平台:一次编写,在 iOS、iPadOS、macOS、watchOS 和 tvOS 上运行
  3. 实时预览:在 Xcode 中实时查看界面效果
  4. 与 Swift 语言深度集成:充分利用 Swift 的类型安全和现代特性
  5. 自动适配:自动处理不同尺寸设备的布局

与 UIKit 的对比

特性 SwiftUI UIKit
编程范式 声明式 命令式
代码风格 简洁、直观 冗长、命令式
布局系统 自动布局,基于栈 Auto Layout,需要手动设置约束
状态管理 自动状态同步 手动更新 UI
跨平台 支持所有 Apple 平台 主要针对 iOS/tvOS
开发效率 相对较低

1.2 Xcode 开发环境配置

系统要求

  • macOS:最新版本(推荐 macOS Sonoma 或更高)
  • Xcode:最新版本(推荐 Xcode 15 或更高)
  • Swift:Swift 5.7 或更高
  • iOS:iOS 15.0 或更高(如果需要支持旧版本,最低可到 iOS 13.0)

安装 Xcode

  1. 打开 App Store
  2. 搜索 “Xcode”
  3. 点击 “获取” 进行安装
  4. 安装完成后,打开 Xcode 并同意许可协议

安装额外组件

首次打开 Xcode 时,会提示安装额外的组件,包括:

  • 命令行工具
  • 模拟器运行时
  • 其他必要的开发工具

1.3 创建你的第一个 SwiftUI 项目

步骤 1:打开 Xcode

步骤 2:创建新项目

  1. 点击 “Create a new Xcode project”
  2. 选择 “iOS” 标签页
  3. 选择 “App” 模板
  4. 点击 “Next”

步骤 3:配置项目信息

  1. Product Name:输入项目名称,例如 “SwiftUIHelloWorld”
  2. Team:选择你的开发团队(如果没有,可以选择 “None”)
  3. Organization Identifier:输入你的组织标识符,例如 “com.yourname”
  4. Interface:选择 “SwiftUI”
  5. Language:选择 “Swift”
  6. Life Cycle:选择 “SwiftUI App”
  7. 取消勾选 “Use Core Data”(暂时不需要)
  8. 点击 “Next”

步骤 4:选择保存位置

选择一个合适的文件夹保存项目,然后点击 “Create”

步骤 5:项目结构介绍

创建完成后,你会看到以下文件结构:

  • SwiftUIHelloWorldApp.swift:应用程序入口
  • ContentView.swift:主视图
  • Assets.xcassets:资源文件
  • Info.plist:应用配置

1.4 认识 Xcode 预览功能

预览面板

Xcode 右侧的预览面板是 SwiftUI 最强大的特性之一,它允许你实时查看界面效果。

使用预览

  1. 打开 ContentView.swift 文件
  2. 确保右侧的预览面板可见(如果不可见,点击 Xcode 顶部的 “Editor” → “Canvas”)
  3. 你会看到 ContentView 的实时预览
  4. 修改代码,预览会自动更新

预览配置

你可以在预览代码中添加多个预览,例如:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
                .previewDisplayName("默认预览")
            
            ContentView()
                .previewDisplayName("暗黑模式")
                .preferredColorScheme(.dark)
            
            ContentView()
                .previewDisplayName("iPhone 15 Pro")
                .previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro"))
        }
    }
}

预览快捷键

  • Option + Command + P:刷新预览
  • Command + K:清除构建

1.5 SwiftUI 项目结构解析

应用程序入口

SwiftUIHelloWorldApp.swift 是应用的入口点,它定义了应用的结构:

import SwiftUI

@main
struct SwiftUIHelloWorldApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

主视图

ContentView.swift 是应用的主视图:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

关键概念解析

  1. @main:标记应用程序的入口点
  2. App 协议:定义应用的结构
  3. Scene:表示应用的一个场景
  4. WindowGroup:创建应用的窗口
  5. View 协议:所有 SwiftUI 视图必须遵循的协议
  6. body:计算属性,返回视图的内容
  7. #Preview:Xcode 15+ 的新语法,用于创建预览

资源管理

  • Assets.xcassets:管理应用的图片、颜色等资源
  • Info.plist:应用的配置信息,如应用名称、版本号等

实战:创建一个简单的欢迎页面

需求分析

创建一个包含以下元素的欢迎页面:

  1. 应用图标
  2. 应用名称
  3. 欢迎标语
  4. 开始按钮

代码实现

import SwiftUI

struct WelcomeView: View {
    // 状态变量,用于控制是否显示欢迎页面
    @State private var isWelcomeShown = true
    
    var body: some View {
        if isWelcomeShown {
            VStack(spacing: 20) {
                // 应用图标
                Image(systemName: "star.fill")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 100, height: 100)
                    .foregroundStyle(.yellow)
                
                // 应用名称
                Text("欢迎使用 SwiftUI")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                
                // 欢迎标语
                Text("一个现代、简洁的 UI 框架")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
                
                // 开始按钮
                Button("开始探索") {
                    // 点击按钮后隐藏欢迎页面
                    isWelcomeShown = false
                }
                .buttonStyle(.borderedProminent)
                .tint(.blue)
            }
            .padding()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(
                LinearGradient(
                    colors: [.blue.opacity(0.1), .purple.opacity(0.1)],
                    startPoint: .top,
                    endPoint: .bottom
                )
            )
        } else {
            // 主内容页面
            VStack {
                Text("探索 SwiftUI 的世界!")
                    .font(.title)
                Text("这里是你的应用主界面")
                    .foregroundStyle(.secondary)
            }
            .padding()
        }
    }
}

#Preview {
    WelcomeView()
}

代码解析

  1. @State:用于创建视图的本地状态
  2. VStack:垂直堆叠视图
  3. Image:显示图片,使用系统图标
  4. Text:显示文本
  5. Button:创建按钮,带有点击动作
  6. LinearGradient:创建线性渐变背景
  7. 条件渲染:使用 if-else 控制显示不同的内容

小结

本章介绍了 SwiftUI 的基本概念和开发环境搭建,包括:

  • SwiftUI 的核心优势和与 UIKit 的对比
  • Xcode 的安装和配置
  • 创建第一个 SwiftUI 项目的步骤
  • Xcode 预览功能的使用
  • SwiftUI 项目的基本结构
  • 一个简单欢迎页面的实现

通过本章的学习,你已经了解了 SwiftUI 的基本概念和开发环境,为后续的学习打下了基础。

参考资料

昨天以前首页

深入剖析 SDAnimatedImageView:如何优雅地在 iOS 中实现高性能动态图渲染

作者 90后晨仔
2026年4月2日 06:28

在日常的 iOS 开发中,动态图(GIF、APNG、WebP)的展示几乎无处不在。然而,很多开发者在使用系统原生的 UIImageView 加载动态图时,往往会遭遇内存暴涨(OOM)或滑动卡顿的窘境。

作为 iOS 圈内最权威的图片处理框架,SDWebImage 为我们提供了一个非常好的解决方案——SDAnimatedImageView

本文将从系统痛点出发,结合 SDWebImage 最新源码,深度拆解 SDAnimatedImageView 的底层架构、核心属性机制,并分享在复杂业务场景下的避坑指南。

文中所涉及源码均基于 SDWebImage 5.x 版本,示例代码采用 Objective-C,Swift 开发者可参照类似逻辑使用。


一、系统原生方案的“三宗罪”

在了解 SDAnimatedImageView 之前,我们必须先明白系统原生方案到底差在哪里。

1. 内存爆炸

系统的 UIImage 在解析 GIF 时,采用“全量解码”策略。
一张体积仅为 2MB 的 GIF,如果包含 50 帧,系统会将其每一帧都解码成庞大的位图对象驻留在内存中。
解码后的位图大小 = 图片宽 × 高 × 4 字节(RGBA)。
假设宽高为 1000×1000,一帧就占约 4MB,50 帧就是 200MB,极易触发 OOM 崩溃。

2. 主线程阻塞

图片的解码过程默认在主线程同步进行,会导致明显的掉帧和卡顿。

3. 控制力极弱

系统几乎没有提供控制 GIF 播放进度、暂停、快进的 API。

SDAnimatedImageView 的诞生,正是为了彻底颠覆这种粗放的渲染模式。


二、核心架构:按需解码与帧缓冲池

SDAnimatedImageView 继承自 UIImageView,但它在内部重构了整个动态图渲染管线。其核心思想是:按需解码,以可控的内存开销换取极致的播放流畅度

1. 零内存的原始数据存储

它配合 SDAnimatedImage 使用。SDAnimatedImage 在初始化时,只保存动态图的原始文件数据(NSData),绝不提前解码任何一帧。此时,无论 GIF 有多少帧,内存占用几乎等于文件本身的大小。

// 从网络或本地获取 NSData
NSData *gifData = [NSData dataWithContentsOfFile:path];
// 创建 SDAnimatedImage,此时仅保留原始数据,不解码
SDAnimatedImage *animatedImage = [SDAnimatedImage imageWithData:gifData];

2. 智能帧缓冲池

当动画开始播放时,它不会一次性解码所有帧,而是维护一个滑动窗口式的缓冲池。在渲染当前帧的同时,后台异步线程会提前解码接下来的几帧放入内存;当某一帧不再处于缓冲窗口内时,其占用的内存会被立即释放。

3. VSync 级别的精准驱动机制

抛弃了传统的 NSTimer(容易受 RunLoop 阻塞影响导致掉帧),SDAnimatedImageView 底层采用了基于 VSync 信号的 CADisplayLink。它与屏幕刷新率完美同步,根据每一帧设定的 duration 精准计算渲染时机,保证动画如丝般顺滑,且在 App 退到后台时自动暂停,不浪费 CPU 资源。


三、源码级 API 解析(核心属性深挖)

很多开发者只把 SDAnimatedImageView 当作普通的 UIImageView 来用,这其实暴殄天物。以下几个核心属性,体现了框架设计的极致细节。

1. 性能调优:maxBufferSizeprefetchNumberOfFrames

@property (nonatomic, assign) NSUInteger maxBufferSize;
@property (nonatomic, assign) NSUInteger prefetchNumberOfFrames;
  • maxBufferSize:最大缓冲区大小(字节)。
    ⚠️ 重要纠正:很多人以为默认值 0 代表“不限制缓冲”,这是错误的!
    根据官方源码注释,0 代表 Auto(自动调整),框架会根据当前设备的内存压力动态计算缓冲上限。
    如果你需要极致的性能,可以设为 NSUIntegerMax(全缓冲,最高性能);如果内存极度吃紧,设为 1(代表无缓冲,最低内存)。

  • prefetchNumberOfFrames:预解码帧数,默认为 3~5 帧。
    增大它可以提高流畅度(尤其在高帧率动图中),但会增加内存;减小则会降低内存占用,但可能在复杂 GIF 时掉帧。
    这个值需要根据业务场景权衡。

2. 运行循环策略:runLoopMode

@property (nonatomic, strong) NSRunLoopMode runLoopMode;

⚠️ 源码纠正:普遍认为它的默认模式是 NSRunLoopCommonModes,但这并不完全准确。
官方源码的默认逻辑其实更智能:

// SDAnimatedImageView.m 中的 commonInit 片段
if ([[NSProcessInfo processInfo] processorCount] > 1) {
    _runLoopMode = NSRunLoopCommonModes;
} else {
    _runLoopMode = NSDefaultRunLoopMode;
}
  • 在多核设备上,默认为 NSRunLoopCommonModes,确保在 UIScrollView 滑动时,GIF 依然能流畅播放(因为滑动时 RunLoop 切换到了 UITrackingRunLoopMode)。
  • 在单核设备(老旧设备)上,默认降级为 NSDefaultRunLoopMode。目的是在滑动时主动暂停 GIF 播放,以节省宝贵的 CPU 资源用来保证列表滑动的流畅度。

3. 进阶播放控制(易被忽略的宝藏属性)

@property (nonatomic, assign) float playbackRate;                       // 播放速率,默认 1.0
@property (nonatomic, assign) BOOL clearBufferWhenStopped;              // 停止时是否清空缓冲池
@property (nonatomic, assign) BOOL shouldIncrementalLoad;               // 是否支持渐进式加载
  • playbackRate:支持 0.5 慢放、2.0 快进。这在实现类似“表情包编辑器”时非常有用。
  • clearBufferWhenStopped:停止动画时是否清空帧缓存(默认 NO)。
    实战意义极大:在复杂的 Feed 流中,当 Cell 滑出屏幕停止播放时,开启此属性可以立即释放掉该 GIF 占用的解码内存,大幅降低峰值内存。
  • shouldIncrementalLoad:是否支持渐进式加载(默认 YES)。
    配合网络下载,即使 GIF 只下载了 30%,它也能立刻播放已下载完成的那部分帧,带来“秒开”的体验。

四、实战:如何正确使用

1. 结合网络加载(最常用)

得益于 SDWebImage 的封装,日常开发中你甚至不需要手动创建 SDAnimatedImage,框架在下载完毕后会自动识别格式并适配。

步骤

  1. 在 Xib/Storyboard 中,将 UIImageViewCustom Class 改为 SDAnimatedImageView
    或纯代码创建:
    SDAnimatedImageView *imageView = [[SDAnimatedImageView alloc] init];
    
  2. 直接使用 sd_setImage 方法:
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"https://example.com/demo.gif"]
                  placeholderImage:[UIImage imageNamed:@"placeholder"]];

原理:SDWebImage 在下载完成后,会根据图片数据判断是否为动图(如检查 GIF 头部 GIF89a),如果是,会自动创建 SDAnimatedImage 实例并赋值给 animatedImage 属性,从而触发按需解码机制。

2. 本地动态图加载

如果是加载 Bundle 或沙盒中的本地数据,必须手动包装为 SDAnimatedImage 才能触发低内存机制:

// 从 Bundle 中获取 GIF 数据
NSString *path = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"gif"];
NSData *gifData = [NSData dataWithContentsOfFile:path];

// 关键步骤:转换为 SDAnimatedImage,保留原始 NSData
SDAnimatedImage *animatedImage = [SDAnimatedImage imageWithData:gifData];

// 赋值给 SDAnimatedImageView
self.imageView.animatedImage = animatedImage;  // 自动开始播放(若 autoPlayAnimatedImage 为 YES)

3. 手动控制播放

如果不想自动播放,可以设置 autoPlayAnimatedImage = NO,然后手动调用:

self.imageView.autoPlayAnimatedImage = NO;
self.imageView.animatedImage = animatedImage;
// 在合适的时机手动开始
[self.imageView startAnimating];

也可以获取当前播放状态:

NSUInteger currentFrame = self.imageView.currentFrameIndex;
NSUInteger currentLoop = self.imageView.currentLoopCount;

五、生产环境“避坑指南”

在将 SDAnimatedImageView 推向线上后,我们踩过几个深坑,这里分享给大家。

坑 1:XIB/Storyboard 忘记改 Class

这是排名第一的线上低级错误。视觉上看不出区别,GIF 也能播放,但内存监控会报警。
只要没有把 Custom Class 改为 SDAnimatedImageView,它底层就会退化为原生的全量解码模式
对策:在创建 ImageView 时,务必确认类型。

坑 2:缓存降级导致的“静态图”Bug

场景:首页用 SDAnimatedImageView 加载并缓存了一个 GIF。进入详情页,由于某些原因使用了原生的 UIImageView 加载同一个 URL。
现象:详情页的 GIF 变成了一张静态图。
原因:SDWebImage 的磁盘缓存中,为了保留 SDAnimatedImage 的特性,存储的是经过优化的特殊格式数据。普通的 UIImageView 从缓存读取后,由于不具备解码动态图的能力,只能显示第一帧。
对策:在项目架构层面,统一动态图加载组件,严禁混用原生 UIImageViewSDAnimatedImageView 加载同一个动态图 URL。

坑 3:WebP 动图不支持

SDAnimatedImageView 默认支持 GIF 和 APNG,但不支持 WebP 动图。如果你需要播放 WebP,必须引入独立的解码器。
正确集成方式(在 Podfile 中):

pod 'SDWebImageWebPCoder'

然后在 App 启动时注册:

#import <SDWebImageWebPCoder/SDImageWebPCoder.h>

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [SDImageCodersManager.sharedManager addCoder:SDImageWebPCoder.sharedCoder];
    return YES;
}

坑 4:长列表内存优化组合拳

在包含大量 GIF 的朋友圈或微博 Feed 流中,建议在 UITableViewCellprepareForReuse 中配合以下设置:

- (void)prepareForReuse {
    [super prepareForReuse];
    
    // 取消正在进行的图片加载
    [self.gifImageView sd_cancelCurrentImageLoad];
    
    // 停止播放并清空缓冲,极大缓解长列表内存压力
    self.gifImageView.clearBufferWhenStopped = YES;
    [self.gifImageView stopAnimating];
}

为什么这样做?

  • sd_cancelCurrentImageLoad 避免复用 Cell 时旧图片加载回调错乱。
  • clearBufferWhenStopped = YES 确保 Cell 离开屏幕后立即释放解码内存。
  • stopAnimating 停止 CADisplayLink 回调,节约 CPU。

坑 5:动画不播放的排查思路

如果 GIF 设置了但不播放,可以按以下顺序检查:

  1. 确认 animatedImage 属性不为 nil(如果是网络加载,检查 sd_setImage 的回调中是否成功)。
  2. 确认 autoPlayAnimatedImage 是否为 YES,或手动调用了 startAnimating
  3. 确认 runLoopMode 是否在当前 RunLoop 模式下被允许(常见于滑动时,若设置为了 NSDefaultRunLoopMode 则滑动时会暂停)。
  4. 确认图片数据是否完整(可尝试用 SDAnimatedImageimages 属性查看帧数)。

六、总结

SDAnimatedImageView 绝不仅仅是一个“能播 GIF 的 ImageView”。它通过 按需解码动态帧缓冲VSync 驱动 以及 设备自适应策略,在内存与性能之间找到了最优解。

理解并善用它的进阶属性(如 maxBufferSize 的 Auto 机制、clearBufferWhenStopped 等),不仅能让你的 App 告别动态图引发的 OOM 崩溃,更能体现出一名 iOS 开发者对底层渲染机制的深刻理解。在动态图渲染这一块,SDAnimatedImageView 依然是当前业界当之无愧的标杆。


互动时间:你在项目中遇到过哪些动态图相关的“奇葩”问题?欢迎在评论区留言,我们一起探讨最佳实践!

❌
❌