SwiftUI基础控件教程

📱 SwiftUI 基础控件教程

从零开始学习 SwiftUI iOS 开发

31个章节 · 24个核心控件 · 实战项目示例 · 完全免费

📖 关于本教程

本教程将介绍SwiftUI中最常用的基础控件,帮助新手快速入门SwiftUI开发。

⚠️ 系统要求:iOS 13.0+ / macOS 10.15+ / Xcode 11.0+

🎯 学习路径建议

🟢 入门篇 → 🟡 核心篇 → 🔵 进阶篇 → 🟣 应用篇 → 🟠 实战篇 → ⚪ 参考篇
     ↓           ↓          ↓          ↓          ↓         ↓
   基础控件     布局控件    交互控件     高级控件    数据网络    扩展内容
    (8章)       (5章)       (4章)     (4章)       (3章)      (7章)
学习阶段 预计时间 学习目标
🟢 入门篇 1-2小时 掌握基础控件,能构建简单界面
🟡 核心篇 1小时 理解布局系统,掌握UI排列
🔵 进阶篇 2-3小时 学习导航和交互,构建多页面应用
🟣 应用篇 1-2小时 使用高级控件,完善应用功能
🟠 实战篇 2-3小时 掌握数据存储和网络请求
⚪ 参考篇 按需查阅 作为开发手册随时查阅

📑 目录

难度说明:★☆☆ 入门级 | ★★☆ 进阶级 | ★★★ 高级级

🟢 基础控件 (入门篇)

  1. 📝 Text 文本控件 ★☆☆ 约5分钟
  2. 🖼️ Image 图片控件 ★☆☆ 约8分钟
  3. 🔘 Button 按钮 ★☆☆ 约6分钟
  4. ✍️ TextField 文本输入框 ★☆☆ 约7分钟
  5. 🔀 Toggle 开关 ★☆☆ 约4分钟
  6. 🎚️ Slider 滑块 ★☆☆ 约5分钟
  7. 🎯 Picker 选择器 ★★☆ 约8分钟
  8. 📅 DatePicker 日期选择器 ★★☆ 约6分钟

🟡 布局控件 (核心篇)

  1. ↕️ VStack 垂直布局 ★☆☆ 约5分钟
  2. ↔️ HStack 水平布局 ★☆☆ 约5分钟
  3. 📚 ZStack 层叠布局 ★☆☆ 约5分钟
  4. 📜 ScrollView 滚动视图 ★★☆ 约8分钟
  5. 📋 List 列表 ★★☆ 约10分钟

🔵 交互控件 (进阶篇)

  1. 🎬 Animation 动画 ★★★ 约12分钟
  2. 🧭 NavigationStack 导航 ★★☆ 约10分钟
  3. 📄 Sheet 弹窗 ★★☆ 约8分钟
  4. ⚠️ Alert 提示框 ★★☆ 约7分钟

🟣 高级控件 (应用篇)

  1. 🧩 TabView 标签栏 ★★☆ 约8分钟
  2. 📝 Form 表单 ★★☆ 约7分钟
  3. 🔍 SearchBar 搜索栏 ★★☆ 约6分钟
  4. 🗺️ MapView 地图 ★★★ 约10分钟

🟠 数据与网络 (实战篇)

  1. 💾 数据持久化 ★★★ 约15分钟
  2. 🌐 网络请求 ★★★ 约12分钟
  3. 🏗️ MVVM 架构 ★★★ 约20分钟

⚪ 扩展内容 (参考篇)

  1. 📦 常用第三方库 ★★☆ 约10分钟
  2. 🎨 颜色参考表 ★☆☆ 约3分钟
  3. ✨ SF Symbols 图标速查 ★☆☆ 约5分钟
  4. 🛠️ 通用修饰符 ★★☆ 约8分钟
  5. ⚡ 性能优化建议 ★★★ 约10分钟
  6. 🐛 常见错误及解决方案 ★★☆ 约8分钟
  7. 💡 实战小项目 ★★★ 约15分钟

1. 📝 Text 文本控件

难度:★☆☆ 阅读时间:约5分钟

Text是SwiftUI中最基础的控件,用于显示文本内容。


📌 常用 API

API 说明 示例 iOS版本
.font(_) 设置字体样式 .font(.largeTitle) iOS 13
.fontWeight(_) 设置字体粗细 .fontWeight(.bold) iOS 13
.foregroundColor(_) 设置文本颜色 .foregroundColor(.blue) iOS 13
.backgroundColor(_) 设置背景颜色 .backgroundColor(.yellow) iOS 13
.bold() 设置粗体 .bold() iOS 13
.italic() 设置斜体 .italic() iOS 13
.underline() 添加下划线 .underline() iOS 13
.strikethrough() 添加删除线 .strikethrough(true) iOS 13
.lineLimit(_) 限制最大行数 .lineLimit(3).lineLimit(nil) iOS 13
.multilineTextAlignment(_) 多行文本对齐 .multilineTextAlignment(.center) iOS 13
.lineSpacing(_) 设置行间距 .lineSpacing(5) iOS 13
.truncationMode(_:) 截断模式 .truncationMode(.tail) iOS 13
.textSelection(_:) 启用文本选择 .textSelection(.enabled) iOS 15

💻 代码示例

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            // 基础文本
            Text("Hello, SwiftUI!")

            // 设置字体大小
            Text("大标题文本")
                .font(.largeTitle)

            // 设置粗体和颜色
            Text("粗体蓝色文本")
                .bold()
                .foregroundColor(.blue)

            // 设置斜体和背景色
            Text("斜体带背景")
                .italic()
                .backgroundColor(.yellow)

            // 多行文本
            Text("这是一段很长的文本,它会自动换行显示。SwiftUI的Text控件会智能处理文本的换行和布局。")
                .lineLimit(nil)
                .frame(width: 200)
        }
    }
}

⚠️ 注意事项

问题 说明 解决方案
文本不换行 默认情况下Text会自动换行 确保父视图有宽度限制
自定义字体不生效 字体未正确导入 检查Info.plist中的字体配置
AttributedString 富文本格式 使用Text(AttributedString) (iOS 15+)

🔙 返回顶部


2. 🖼️ Image 图片控件

难度:★☆☆ 阅读时间:约8分钟

Image用于显示图片资源。


📌 常用 API

API 说明 示例 iOS版本
Image(_:) 从Assets加载图片 Image("logo") iOS 13
Image(systemName:) 使用SF Symbols图标 Image(systemName: "heart.fill") iOS 13
.resizable() 使图片可调整大小 .resizable() iOS 13
.aspectRatio(_:contentMode:) 设置宽高比模式 .aspectRatio(contentMode: .fit) iOS 13
.scaledToFit() 保持比例适应 .scaledToFit() iOS 13
.scaledToFill() 保持比例填充 .scaledToFill() iOS 13
.frame(_:) 设置图片尺寸 .frame(width: 200, height: 200) iOS 13
.cornerRadius(_:) 设置圆角 .cornerRadius(20) iOS 13
.clipShape(_:) 裁剪成指定形状 .clipShape(Circle()) iOS 13
.renderingMode(_:) 设置渲染模式 .renderingMode(.template) iOS 13
.asyncImage(url:) 异步加载网络图片 .asyncImage(url: imageURL) iOS 15+

💻 代码示例

import SwiftUI

struct ImageView: View {
    var body: some View {
        VStack(spacing: 20) {
            // 从Assets加载图片
            Image("logo")

            // 使用系统图标
            Image(systemName: "heart.fill")
                .font(.system(size: 50))

            // 设置图标颜色
            Image(systemName: "star.fill")
                .font(.largeTitle)
                .foregroundColor(.yellow)

            // 调整图片大小和圆角
            Image("photo")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 200, height: 200)
                .cornerRadius(20)

            // 圆形头像
            Image(systemName: "person.fill")
                .font(.system(size: 80))
                .foregroundColor(.gray)
                .clipShape(Circle())
        }
    }
}

⚠️ 注意事项

问题 说明 解决方案
图片不显示 Assets中的图片名称错误 检查图片名称是否正确
图片变形 使用.resizable()但未设置aspectRatio 配合.aspectRatio().scaledToFit()使用
SF Symbols图标不显示 iOS版本不支持该图标 更换为更早版本的图标

🔙 返回顶部


3. 🔘 Button 按钮

难度:★☆☆ 阅读时间:约6分钟

Button用于响应用户的点击操作。


📌 常用 API

API 说明 示例 iOS版本
Button("标题", action:) 创建文本按钮 Button("点击", action: {}) iOS 13
Button(action:label:) 自定义按钮 Button(action: {}) { Text("点击") } iOS 13
.buttonStyle(_:) 设置按钮样式 .buttonStyle(.bordered) iOS 13
.disabled(_:) 设置是否可用 .disabled(true) iOS 13
.padding() 添加内边距 .padding() iOS 13
.background(_:) 设置背景 .background(Color.blue) iOS 13
.cornerRadius(_:) 设置圆角 .cornerRadius(10) iOS 13
.buttonBorderShape(_:) 按钮边框形状 .buttonBorderShape(.roundedRectangle) iOS 15

🎨 ButtonStyle 样式

样式 说明 iOS版本
.bordered 标准边框样式 iOS 15
.borderedProminent 突出显示样式 iOS 15
.borderless 无边框样式 iOS 15
.plain 简约样式 iOS 13

💻 代码示例

import SwiftUI

struct ButtonView: View {
    @State private var counter = 0

    var body: some View {
        VStack(spacing: 20) {
            // 基础按钮
            Button("点击我") {
                print("按钮被点击了")
            }

            // 自定义样式按钮
            Button(action: {
                counter += 1
            }) {
                Text("计数: \(counter)")
                    .font(.title)
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.blue)
                    .cornerRadius(10)
            }

            // 系统样式按钮
            Button("系统按钮") {
                print("使用系统样式")
            }
            .buttonStyle(.bordered)

            // 图标按钮
            Button(action: {
                print("添加")
            }) {
                HStack {
                    Image(systemName: "plus.circle.fill")
                    Text("添加项目")
                }
            }

            // 禁用状态的按钮
            Button("禁用按钮") {
                print("不会执行")
            }
            .disabled(true)
            .buttonStyle(.bordered)
        }
    }
}

⚠️ 注意事项

问题 说明 解决方案
Button的action不执行 按钮被disabled或在外面加上了手势 检查disabled状态,移除冲突的手势
自定义样式不生效 修饰符顺序错误 确保修饰符顺序正确

🔙 返回顶部


4. ✍️ TextField 文本输入框

难度:★☆☆ 阅读时间:约7分钟

TextField用于接收用户输入的单行文本。


📌 常用 API

API 说明 示例 iOS版本
TextField("占位符", text:) 创建文本框 TextField("用户名", text: $name) iOS 13
SecureField("占位符", text:) 创建密码框 SecureField("密码", text: $pwd) iOS 13
.textFieldStyle(_:) 设置文本框样式 .textFieldStyle(.roundedBorder) iOS 13
.padding() 添加内边距 .padding() iOS 13
.autocapitalization(_:) 自动大写设置 .autocapitalization(.words) iOS 13
.keyboardType(_:) 设置键盘类型 .keyboardType(.emailAddress) iOS 13
.disabled(_:) 禁用输入 .disabled(true) iOS 13
.onSubmit(_:) 提交时执行 .onSubmit { print("提交") } iOS 15

🎨 TextFieldStyle 样式

样式 说明 iOS版本
.roundedBorder 圆角边框样式 iOS 13
.plain 简约样式 iOS 13
.automatic 自动样式 iOS 13

⌨️ KeyboardType 键盘类型

类型 说明 iOS版本
.default 默认键盘 iOS 13
.numberPad 数字键盘 iOS 13
.decimalPad 带小数点的数字键盘 iOS 13
.emailAddress 邮件键盘 iOS 13
.URL URL键盘 iOS 13

💻 代码示例

import SwiftUI

struct TextFieldView: View {
    @State private var username: String = ""
    @State private var password: String = ""

    var body: some View {
        VStack(spacing: 20) {
            // 基础文本框
            TextField("请输入用户名", text: $username)
                .textFieldStyle(.roundedBorder)
                .padding()

            // 显示输入内容
            Text("你输入的是: \(username)")

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

            // 带图标的文本框
            HStack {
                Image(systemName: "envelope.fill")
                    .foregroundColor(.gray)
                TextField("邮箱地址", text: $username)
                    .keyboardType(.emailAddress)
            }
            .padding()
            .background(Color.gray.opacity(0.1))
            .cornerRadius(10)
        }
    }
}

⚠️ 注意事项

问题 说明 解决方案
键盘遮挡输入框 键盘弹出时挡住输入框 使用.toolbar配合ToolbarItemGroup(placement: .keyboard)
输入为空时校验 文本框内容为空 使用.trimmingCharacters(in: .whitespaces)去除空格

🔙 返回顶部


5. 🔀 Toggle 开关

难度:★☆☆ 阅读时间:约4分钟

Toggle用于表示布尔值的开关控件。


📌 常用 API

API 说明 示例 iOS版本
Toggle("标题", isOn:) 创建开关 Toggle("开启", isOn: $isOn) iOS 13
Toggle(isOn:label:) 自定义开关 Toggle(isOn: $flag) { Text("自定义") } iOS 13
.tint(_:) 设置开关颜色 .tint(.red) iOS 14

💻 代码示例

import SwiftUI

struct ToggleView: View {
    @State private var isOn = false
    @State private var notificationsEnabled = true

    var body: some View {
        VStack(spacing: 20) {
            // 基础开关
            Toggle("开启功能", isOn: $isOn)
                .padding()

            // 显示开关状态
            Text("当前状态: \(isOn ? "开启" : "关闭")")

            // 自定义样式的开关
            Toggle(isOn: $notificationsEnabled) {
                HStack {
                    Image(systemName: "bell.fill")
                        .foregroundColor(.orange)
                    VStack(alignment: .leading) {
                        Text("推送通知")
                            .font(.headline)
                        Text("接收每日提醒")
                            .font(.caption)
                            .foregroundColor(.gray)
                    }
                }
            }
            .padding()
            .background(Color.gray.opacity(0.1))
            .cornerRadius(10)
        }
    }
}

⚠️ 注意事项

问题 说明 解决方案
Toggle颜色不生效 iOS 13不支持.tint() 使用.accentColor()替代 (iOS 13) 或升级到iOS 14+

🔙 返回顶部


6. 🎚️ Slider 滑块

难度:★☆☆ 阅读时间:约5分钟

Slider用于在一个范围内选择数值。


📌 常用 API

API 说明 示例 iOS版本
Slider(value:in:) 创建滑块 Slider(value: $val, in: 0...100) iOS 13
Slider(value:in:step:) 带步进的滑块 Slider(value: $val, in: 0...100, step: 1) iOS 13
.accentColor(_:) 设置滑块颜色 .accentColor(.red) iOS 13

💻 代码示例

import SwiftUI

struct SliderView: View {
    @State private var sliderValue: Double = 50
    @State private var temperature: Double = 22

    var body: some View {
        VStack(spacing: 30) {
            // 基础滑块(范围0-100)
            VStack {
                Text("值: \(Int(sliderValue))")
                Slider(value: $sliderValue, in: 0...100)
                    .padding()
            }

            // 带步进的滑块
            VStack {
                HStack {
                    Text("温度")
                    Spacer()
                    Text("\(Int(temperature))°C")
                        .bold()
                }
                Slider(value: $temperature, in: 16...30, step: 1)
                    .accentColor(.red)
            }
            .padding()
        }
    }
}

🔙 返回顶部


7. 🎯 Picker 选择器

难度:★★☆ 阅读时间:约8分钟

Picker用于从多个选项中选择一个。


📌 常用 API

API 说明 示例 iOS版本
Picker("标题", selection:) 创建选择器 Picker("颜色", selection: $index) iOS 13
.pickerStyle(_:) 设置选择器样式 .pickerStyle(.segmented) iOS 13
.tag(_:) 设置选项标识 .tag(0) iOS 13

🎨 PickerStyle 样式

样式 说明 iOS版本
.segmented 分段控件样式 iOS 13
.menu 下拉菜单样式 iOS 14
.wheel 滚轮样式(iOS) iOS 13
.inline 内联样式 iOS 14

💻 代码示例

import SwiftUI

struct PickerView: View {
    @State private var selectedColor = 0
    @State private var selectedSize = "中"

    let colors = ["红色", "绿色", "蓝色", "黄色"]
    let sizes = ["小", "中", "大"]

    var body: some View {
        VStack(spacing: 30) {
            // 分段选择器
            VStack {
                Text("选择颜色: \(colors[selectedColor])")
                Picker("颜色", selection: $selectedColor) {
                    ForEach(0..<colors.count, id: \.self) { index in
                        Text(colors[index]).tag(index)
                    }
                }
                .pickerStyle(.segmented)
            }

            // 菜单样式选择器
            VStack {
                Text("选择尺寸: \(selectedSize)")
                Picker("尺寸", selection: $selectedSize) {
                    ForEach(sizes, id: \.self) { size in
                        Text(size).tag(size)
                    }
                }
                .pickerStyle(.menu)
            }
        }
    }
}

🔙 返回顶部


8. 📅 DatePicker 日期选择器

难度:★★☆ 阅读时间:约6分钟

DatePicker用于选择日期和时间。


📌 常用 API

API 说明 示例 iOS版本
DatePicker("标题", selection:) 创建日期选择器 DatePicker("日期", selection: $date) iOS 13
displayedComponents: 设置显示组件 displayedComponents: .date iOS 13
in: 限制日期范围 in: Date()... iOS 13
.datePickerStyle(_:) 设置选择器样式 .datePickerStyle(.compact) iOS 13

🎨 displayedComponents 选项

选项 说明 iOS版本
.date 只显示日期 iOS 13
.hourAndMinute 只显示时间 iOS 13
[.date, .hourAndMinute] 显示日期和时间 iOS 13

🎨 DatePickerStyle 样式

样式 说明 iOS版本
.compact 紧凑样式 iOS 14
.graphical 日历样式 iOS 14
.wheel 滚轮样式(iOS) iOS 13
.automatic 自动样式 iOS 14

💻 代码示例

import SwiftUI

struct DatePickerView: View {
    @State private var selectedDate = Date()
    @State private var birthDate = Date()

    var body: some View {
        VStack(spacing: 30) {
            // 基础日期选择器
            VStack(alignment: .leading) {
                Text("选择日期")
                    .font(.headline)
                DatePicker("日期", selection: $selectedDate)
                    .datePickerStyle(.compact)
            }

            // 只选日期不选时间
            DatePicker("出生日期", selection: $birthDate, displayedComponents: .date)
                .datePickerStyle(.graphical)

            // 限制日期范围
            VStack {
                DatePicker("预约日期", selection: $selectedDate, in: Date()...)
                    .datePickerStyle(.compact)
                Text("只能选择今天及以后的日期")
                    .font(.caption)
                    .foregroundColor(.gray)
            }
        }
    }
}

🔙 返回顶部


9. ↕️ VStack 垂直布局

难度:★☆☆ 阅读时间:约5分钟

VStack用于将子视图垂直排列。


📌 常用 API

API 说明 示例 iOS版本
VStack(spacing:) 创建垂直布局 VStack(spacing: 20) iOS 13
VStack(alignment:spacing:) 指定对齐方式 VStack(alignment: .leading) iOS 13
.padding() 添加外边距 .padding() iOS 13
.frame(_:) 设置尺寸 .frame(height: 200) iOS 13

🎨 alignment 对齐选项

选项 说明
.leading 左对齐
.center 居中对齐(默认)
.trailing 右对齐

💻 代码示例

import SwiftUI

struct VStackView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("第一行")
                .font(.title)

            Text("第二行")
                .foregroundColor(.blue)

            Text("第三行")
                .foregroundColor(.red)
        }
        .padding()
        .background(Color.gray.opacity(0.1))
    }
}

🔙 返回顶部


10. ↔️ HStack 水平布局

难度:★☆☆ 阅读时间:约5分钟

HStack用于将子视图水平排列。


📌 常用 API

API 说明 示例 iOS版本
HStack(spacing:) 创建水平布局 HStack(spacing: 15) iOS 13
HStack(alignment:spacing:) 指定对齐方式 HStack(alignment: .top) iOS 13
Spacer() 弹性空间 Spacer() iOS 13

🎨 alignment 对齐选项

选项 说明
.top 顶部对齐
.center 居中对齐(默认)
.bottom 底部对齐

💻 代码示例

import SwiftUI

struct HStackView: View {
    var body: some View {
        VStack(spacing: 30) {
            // 基础水平布局
            HStack(spacing: 15) {
                Text("左边")
                Spacer()
                Text("右边")
            }
            .padding()
            .background(Color.blue.opacity(0.1))
        }
    }
}

🔙 返回顶部


11. 📚 ZStack 层叠布局

难度:★☆☆ 阅读时间:约5分钟

ZStack用于将子视图层叠在一起。


📌 常用 API

API 说明 示例 iOS版本
ZStack() 创建层叠布局 ZStack() iOS 13
ZStack(alignment:) 指定对齐方式 ZStack(alignment: .center) iOS 13

🎨 alignment 对齐选项

选项 说明
.topLeading 左上角
.top 顶部居中
.topTrailing 右上角
.leading 左侧居中
.center 居中(默认)
.trailing 右侧居中
.bottomLeading 左下角
.bottom 底部居中
.bottomTrailing 右下角

💻 代码示例

import SwiftUI

struct ZStackView: View {
    var body: some View {
        VStack(spacing: 30) {
            // 基础层叠布局
            ZStack {
                Circle()
                    .fill(Color.blue)
                    .frame(width: 150, height: 150)

                Circle()
                    .fill(Color.green)
                    .frame(width: 100, height: 100)

                Text("中心")
                    .font(.title)
                    .foregroundColor(.white)
            }
        }
    }
}

🔙 返回顶部


12. 📜 ScrollView 滚动视图

难度:★★☆ 阅读时间:约8分钟

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


📌 常用 API

API 说明 示例 iOS版本
ScrollView() 创建垂直滚动视图 ScrollView() iOS 13
ScrollView(.horizontal) 创建水平滚动视图 ScrollView(.horizontal) iOS 13
ScrollView([.horizontal, .vertical]) 双向滚动 ScrollView([.horizontal, .vertical]) iOS 13
.scrollDisabled(_:) 禁用滚动 .scrollDisabled(true) iOS 13
.showsIndicators(_:) 显示/隐藏滚动条 .showsIndicators(false) iOS 13

💻 代码示例

import SwiftUI

struct ScrollViewView: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 15) {
                ForEach(1...20, id: \.self) { index in
                    Text("项目 \(index)")
                        .font(.title)
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.blue.opacity(Double(index) / 20))
                        .cornerRadius(10)
                }
            }
            .padding()
        }
    }
}

🔙 返回顶部


13. 📋 List 列表

难度:★★☆ 阅读时间:约10分钟

List用于显示数据列表,支持滚动和分组。


📌 常用 API

API 说明 示例 iOS版本
List {} 创建静态列表 List { Text("项目") } iOS 13
List(data, id: \.self) 创建动态列表 List(items, id: \.self) iOS 13
Section(header:) 创建分组 Section(header: Text("标题")) iOS 13
Section(footer:) 设置组尾 Section(footer: Text("结尾")) iOS 13
.listStyle(_:) 设置列表样式 .listStyle(.insetGrouped) iOS 13
.listRowSeparator(_:) 设置分隔线 .listRowSeparator(.hidden) iOS 13
.swipeActions(edge:allowsFullSwipe:) 滑动操作 .swipeActions(edge: .trailing) iOS 14

🎨 listStyle 样式

样式 说明 iOS版本
.automatic 自动样式(默认) iOS 14
.insetGrouped 分组样式,带边距 iOS 13
.plain 简约样式 iOS 13
.inset 带边距的简约样式 iOS 13

💻 代码示例

import SwiftUI

struct ListView: View {
    let fruits = ["苹果", "香蕉", "橙子", "葡萄", "西瓜"]

    var body: some View {
        List {
            Section(header: Text("第一组")) {
                Text("项目1")
                Text("项目2")
            }

            Section(header: Text("第二组")) {
                Text("项目3")
                Text("项目4")
            }
        }
        .listStyle(.insetGrouped)
    }
}

🔙 返回顶部


14. 🎬 Animation 动画

难度:★★★ 阅读时间:约12分钟

SwiftUI 提供了简单而强大的动画系统。


📌 常用 API

API 说明 示例 iOS版本
.animation(_:) 设置动画 .animation(.default) iOS 13
.transition(_:) 设置过渡效果 .transition(.slide) iOS 13
.withAnimation(_:) 执行动画 .withAnimation { } iOS 13
.animation(_:value:) 值变化时触发动画 .animation(.spring(), value: offset) iOS 15

🎨 内置动画类型

类型 说明 iOS版本
.default 默认动画 iOS 13
.easeIn 缓入 iOS 13
.easeOut 缓出 iOS 13
.easeInOut 缓入缓出 iOS 13
.linear 线性 iOS 13
.spring() 弹簧动画 iOS 13

🎭 过渡效果 Transition

效果 说明 iOS版本
.opacity 透明度渐变 iOS 13
.scale 缩放 iOS 13
.slide 滑动 iOS 13
.move(edge:) 从边缘移动 iOS 13

💻 代码示例

import SwiftUI

struct AnimationView: View {
    @State private var isShowing = false
    @State private var scale: CGFloat = 1.0
    @State private var offset: CGFloat = 0

    var body: some View {
        VStack(spacing: 30) {
            // 基础动画
            VStack {
                Circle()
                    .fill(isShowing ? Color.blue : Color.gray)
                    .frame(width: 100, height: 100)
                    .animation(.easeInOut(duration: 0.5), value: isShowing)

                Button("切换颜色") {
                    isShowing.toggle()
                }
            }

            // 缩放动画
            VStack {
                Circle()
                    .fill(Color.green)
                    .frame(width: 80 * scale, height: 80 * scale)
                    .animation(.spring(response: 0.3, dampingFraction: 0.6), value: scale)

                HStack(spacing: 20) {
                    Button("缩小") {
                        scale = max(0.5, scale - 0.2)
                    }
                    Button("放大") {
                        scale = min(2.0, scale + 0.2)
                    }
                }
            }

            // 位移动画
            VStack {
                Circle()
                    .fill(Color.orange)
                    .frame(width: 60, height: 60)
                    .offset(x: offset)
                    .animation(.spring(), value: offset)

                Slider(value: $offset, in: -100...100)
                    .frame(width: 200)
            }

            // 过渡动画
            VStack {
                if isShowing {
                    RoundedRectangle(cornerRadius: 20)
                        .fill(Color.purple)
                        .frame(width: 150, height: 150)
                        .transition(.asymmetric(
                            insertion: .scale,
                            removal: .opacity
                        ))
                }

                Button("显示/隐藏") {
                    withAnimation(.spring()) {
                        isShowing.toggle()
                    }
                }
            }
        }
        .padding()
    }
}

// 组合动画示例
struct ComboAnimationView: View {
    @State private var isAnimating = false

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: isAnimating ? 50 : 10)
                .fill(isAnimating ? Color.blue : Color.orange)
                .frame(width: isAnimating ? 150 : 100, height: isAnimating ? 150 : 100)
                .rotationEffect(.degrees(isAnimating ? 360 : 0))
                .opacity(isAnimating ? 1.0 : 0.5)
                .animation(.spring(response: 0.6, dampingFraction: 0.5), value: isAnimating)

            Button("播放组合动画") {
                isAnimating.toggle()
            }
        }
        .padding()
    }
}

⚠️ 注意事项

问题 说明 解决方案
动画不生效 缺少依赖值 使用 animation(_:value:) 并绑定正确的值
iOS 16+ 动画API 新版本API变更 使用 .animation(_:value:) 替代旧的 .animation(_:)

🔙 返回顶部


15. 🧭 NavigationStack 导航

难度:★★☆ 阅读时间:约10分钟

⚠️ 注意:NavigationStack 需要 iOS 16.0+,旧版本请使用 NavigationView

NavigationStack(iOS 16+)是 NavigationView 的升级版,用于构建多层级导航界面。

📌 常用 API

API 说明 示例 iOS版本
NavigationStack 创建导航栈 NavigationStack { } iOS 16
NavigationLink(value:) 导航链接 NavigationLink(value: item) { } iOS 16
.navigationTitle(_:) 设置标题 .navigationTitle("标题") iOS 13
.navigationBarTitleDisplayMode(_:) 标题显示模式 .navigationBarTitleDisplayMode(.inline) iOS 13
.toolbar(_:) 工具栏 .toolbar { ToolbarItem } iOS 14
.navigationDestination(_:) 导航目标 .navigationDestination(for: Item.self) iOS 16

🎨 标题显示模式

模式 说明 iOS版本
.automatic 自动(大标题) iOS 13
.inline 小标题居中 iOS 13
.large 大标题 iOS 13

💻 代码示例

import SwiftUI

// 数据模型
struct Destination: Hashable {
    let id: Int
    let name: String
}

struct NavigationStackView: View {
    @State private var path = [Destination]()

    var body: some View {
        NavigationStack(path: $path) {
            List {
                // 导航链接
                Section("基础导航") {
                    NavigationLink("详情页面", value: Destination(id: 1, name: "详情"))
                    NavigationLink("设置页面", value: Destination(id: 2, name: "设置"))
                }

                // 使用 Button + programmatic navigation
                Section("程序化导航") {
                    Button("跳转到关于") {
                        path.append(Destination(id: 3, name: "关于"))
                    }

                    Button("返回上一级") {
                        _ = path.popLast()
                    }

                    Button("返回根视图") {
                        path.removeAll()
                    }
                }

                // 嵌套导航
                Section("嵌套列表") {
                    ForEach(1...5, id: \.self) { i in
                        NavigationLink("项目 \(i)", value: Destination(id: i, name: "项目\(i)"))
                    }
                }
            }
            .navigationTitle("NavigationStack")
            .navigationBarTitleDisplayMode(.large)
            .navigationDestination(for: Destination.self) { destination in
               DetailView(destination: destination)
            }
        }
    }
}

// 详情视图
struct DetailView: View {
    let destination: Destination
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "star.fill")
                .font(.system(size: 60))
                .foregroundColor(.yellow)

            Text(destination.name)
                .font(.largeTitle)

            Text("ID: \(destination.id)")
                .foregroundColor(.secondary)

            Button("关闭") {
                dismiss()
            }
            .buttonStyle(.bordered)
        }
        .navigationTitle(destination.name)
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                Button("分享") {
                    print("分享")
                }
            }
        }
    }
}

// 传统 NavigationView 写法(iOS 13+)
struct NavigationViewExample: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink("页面1") {
                    Text("页面1内容")
                        .navigationTitle("页面1")
                }
                NavigationLink("页面2") {
                    Text("页面2内容")
                        .navigationTitle("页面2")
                }
            }
            .navigationTitle("NavigationView")
        }
    }
}

⚠️ 注意事项

问题 说明 解决方案
iOS 16 以下不支持 NavigationStack 是 iOS 16+ 使用 NavigationView 代替
path 不工作 需要绑定 path 参数 使用 @State private var path

🔙 返回顶部


16. 📄 Sheet 弹出视图

难度:★★☆ 阅读时间:约8分钟

Sheet 用于从底部弹出半屏或全屏视图。


📌 常用 API

API 说明 示例 iOS版本
.sheet(isPresented:) 弹出 Sheet .sheet(isPresented: $show) iOS 13
.sheet(item:) 使用 Item 弹出 .sheet(item: $selectedItem) iOS 13
.presentationDetents(_:) 设置 Sheet 高度 .presentationDetents([.medium]) iOS 16
.presentationDragIndicator(_:) 拖动指示器 .presentationDragIndicator(.visible) iOS 16
.interactiveDismissDisabled(_:) 禁用下拉关闭 .interactiveDismissDisabled(true) iOS 15

🎨 Sheet 高度选项

选项 说明 iOS版本
.large 全屏(默认) iOS 16
.medium 半屏 iOS 16
.fraction(_:) 自定义比例 iOS 16
.height(_:) 自定义高度 iOS 16

💻 代码示例

import SwiftUI

struct SheetView: View {
    @State private var showSheet = false
    @State private var selectedItem: Item?
    @State private var showCustomSheet = false

    var body: some View {
        VStack(spacing: 30) {
            // 基础 Sheet
            Button("显示 Sheet") {
                showSheet = true
            }
            .buttonStyle(.borderedProminent)
            .sheet(isPresented: $showSheet) {
                SheetContentView()
            }

            // 带 Item 的 Sheet
            Button("显示详情") {
                selectedItem = Item(id: 1, name: "示例项目")
            }
            .buttonStyle(.bordered)
            .sheet(item: $selectedItem) { item in
                VStack(spacing: 20) {
                    Text(item.name)
                        .font(.title)
                    Text("ID: \(item.id)")
                    Button("关闭") {
                        selectedItem = nil
                    }
                }
                .padding()
            }

            // 自定义高度 Sheet (iOS 16+)
            Button("显示半屏 Sheet") {
                showCustomSheet = true
            }
            .buttonStyle(.bordered)
            .sheet(isPresented: $showCustomSheet) {
                VStack {
                    Text("这是半屏 Sheet")
                        .font(.title)
                    Text("可以下拉关闭")
                    Button("关闭") {
                        showCustomSheet = false
                    }
                    .buttonStyle(.borderedProminent)
                }
                .padding()
                .presentationDetents([.medium, .large])
                .presentationDragIndicator(.visible)
            }
        }
        .padding()
    }
}

// Sheet 内容视图
struct SheetContentView: View {
    @Environment(\.dismiss) private var dismiss
    @State private var text = ""

    var body: some View {
        NavigationView {
            Form {
                Section("输入信息") {
                    TextField("请输入内容", text: $text)
                }
            }
            .navigationTitle("Sheet")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("取消") {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("保存") {
                        // 保存逻辑
                        dismiss()
                    }
                    .disabled(text.isEmpty)
                }
            }
        }
    }
}

// 数据模型
struct Item: Identifiable {
    let id: Int
    let name: String
}

⚠️ 注意事项

问题 说明 解决方案
Sheet 高度设置无效 iOS 16+ 才支持 检查 iOS 版本,使用 @available(iOS 16, *)
下拉关闭不工作 可能被禁用 检查 .interactiveDismissDisabled()

🔙 返回顶部


17. ⚠️ Alert 提示框

难度:★★☆ 阅读时间:约7分钟

Alert 用于显示重要信息或需要用户确认的操作。


📌 常用 API

API 说明 示例 iOS版本
.alert(_:) 显示 Alert .alert("标题", isPresented: $show) iOS 13
Alert(_:) 创建 Alert Alert(title: Text("标题")) iOS 13
.confirmationDialog(_:) 确认对话框 .confirmationDialog("标题", isPresented: $show) iOS 15

💻 代码示例

import SwiftUI

struct AlertView: View {
    @State private var showAlert = false
    @State private var showAlert2 = false
    @State private var showDeleteAlert = false
    @State private var showConfirmation = false
    @State private var alertMessage = ""

    var body: some View {
        VStack(spacing: 30) {
            // 基础 Alert
            Button("显示 Alert") {
                alertMessage = "这是一个提示信息"
                showAlert = true
            }
            .buttonStyle(.borderedProminent)
            .alert("提示", isPresented: $showAlert) {
                Button("确定") {}
            } message: {
                Text(alertMessage)
            }

            // 带多个按钮的 Alert
            Button("显示多按钮 Alert") {
                showAlert2 = true
            }
            .buttonStyle(.bordered)
            .alert("选择操作", isPresented: $showAlert2) {
                Button("取消", role: .cancel) {}
                Button("确定") {}
                Button("删除", role: .destructive) {}
            } message: {
                Text("请选择要执行的操作")
            }

            // 删除确认 Alert
            Button("删除项目") {
                showDeleteAlert = true
            }
            .buttonStyle(.bordered)
            .foregroundColor(.red)
            .alert("确认删除", isPresented: $showDeleteAlert) {
                Button("取消", role: .cancel) {}
                Button("删除", role: .destructive) {
                    // 执行删除操作
                    print("已删除")
                }
            } message: {
                Text("此操作无法撤销,确定要删除吗?")
            }

            // ConfirmationDialog (iOS 15+)
            Button("显示确认对话框") {
                showConfirmation = true
            }
            .buttonStyle(.bordered)
            .confirmationDialog("选择颜色", isPresented: $showConfirmation, titleVisibility: .visible) {
                Button("红色") { print("选择红色") }
                Button("绿色") { print("选择绿色") }
                Button("蓝色") { print("选择蓝色") }
                Button("取消", role: .cancel) {}
            } message: {
                Text("请选择一个颜色")
            }
        }
        .padding()
    }
}

// 自定义 Alert 样式
struct CustomAlertView: View {
    @State private var showError = false
    @State private var showSuccess = false
    @State private var showWarning = false

    var body: some View {
        VStack(spacing: 20) {
            // 成功提示
            Button("成功提示") {
                showSuccess = true
            }
            .alert("成功", isPresented: $showSuccess) {
                Button("好的") {}
            } message: {
                Text("操作已完成!")
            }

            // 错误提示
            Button("错误提示") {
                showError = true
            }
            .alert("错误", isPresented: $showError) {
                Button("重试") {}
                Button("取消", role: .cancel) {}
            } message: {
                Text("操作失败,请重试")
            }

            // 警告提示
            Button("警告提示") {
                showWarning = true
            }
            .alert("警告", isPresented: $showWarning) {
                Button("继续", role: .destructive) {}
                Button("取消", role: .cancel) {}
            } message: {
                Text("此操作可能有风险")
            }
        }
        .buttonStyle(.bordered)
    }
}

⚠️ 注意事项

问题 说明 解决方案
Alert 不显示 isPresented 绑定错误 确保使用 $ 绑定 @State 变量
多个 Alert 冲突 同一视图多个 Alert 可能只显示最后一个 使用不同的状态变量或组合 Alert

🔙 返回顶部


18. 🧩 TabView 标签栏

难度:★★☆ 阅读时间:约8分钟

TabView 用于创建底部标签栏导航,是多页面应用的核心组件。


📌 常用 API

API 说明 示例 iOS版本
TabView(selection:) 创建标签栏 TabView(selection: $tabIndex) iOS 14
.tabItem(_:) 设置标签项 .tabItem { Label("首页", systemImage: "house") } iOS 13
.badge(_:) 设置角标 .badge(5) iOS 15
.badgeProminence(_:) 角标 prominence .badgeProminence(.increased) iOS 17

💻 代码示例

import SwiftUI

struct TabViewExample: View {
    @State private var selectedTab = 0

    var body: some View {
        TabView(selection: $selectedTab) {
            // 首页
            HomeView()
                .tabItem {
                    Label("首页", systemImage: "house.fill")
                }
                .tag(0)

            // 搜索
            SearchView()
                .tabItem {
                    Label("搜索", systemImage: "magnifyingglass")
                }
                .tag(1)

            // 通知(带角标)
            NotificationView()
                .tabItem {
                    Label("通知", systemImage: "bell.fill")
                }
                .badge(99)
                .tag(2)

            // 个人中心
            ProfileView()
                .tabItem {
                    Label("我的", systemImage: "person.fill")
                }
                .tag(3)
        }
        .tint(.blue)
    }
}

// 各个页面视图
struct HomeView: View {
    var body: some View {
        NavigationView {
            List {
                Text("首页内容")
            }
            .navigationTitle("首页")
        }
    }
}

struct SearchView: View {
    var body: some View {
        Text("搜索页面")
            .navigationTitle("搜索")
    }
}

struct NotificationView: View {
    var body: some View {
        List {
            Text("通知1")
            Text("通知2")
            Text("通知3")
        }
        .navigationTitle("通知")
    }
}

struct ProfileView: View {
    var body: some View {
        List {
            HStack {
                Image(systemName: "person.circle.fill")
                    .font(.largeTitle)
                VStack(alignment: .leading) {
                    Text("用户名")
                        .font(.headline)
                    Text("查看个人资料")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }
        }
        .navigationTitle("我的")
    }
}

// 带自定义样式的 TabView
struct CustomTabView: View {
    @State private var selectedTab = "home"

    var body: some View {
        TabView(selection: $selectedTab) {
            ContentView()
                .tabItem {
                    Label("首页", systemImage: "house.fill")
                }
                .tag("home")

            SettingsView()
                .tabItem {
                    Label("设置", systemImage: "gearshape.fill")
                }
                .tag("settings")
        }
    }
}

// PageStyle TabView(轮播图)
struct PageStyleTabView: View {
    @State private var currentPage = 0

    var body: some View {
        VStack {
            TabView(selection: $currentPage) {
                ForEach(0..<5) { index in
                    VStack {
                        Text("页面 \(index + 1)")
                            .font(.largeTitle)
                            .foregroundColor(.white)
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.blue.opacity(Double(index) / 5))
                    .tag(index)
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .always))
            .frame(height: 200)

            Text("当前页面: \(currentPage + 1)")
        }
    }
}

⚠️ 注意事项

问题 说明 解决方案
TabView 不显示 Tab 内容未使用 .tabItem() 确保每个子视图都有 .tabItem()
badge 不显示 iOS 版本过低 badge 需要 iOS 15+
标签图标不显示 systemName 错误 检查 SF Symbols 名称是否正确

🔙 返回顶部


19. 📝 Form 表单

难度:★★☆ 阅读时间:约7分钟

Form 是用于构建设置页面、表单输入的专用容器,提供分组样式。


📌 常用 API

API 说明 示例 iOS版本
Form {} 创建表单 Form { } iOS 13
Section(header:) 创建分组 Section(header: Text("标题")) iOS 13
Section(footer:) 设置组尾 Section(footer: Text("说明")) iOS 13
.disabled(_:) 禁用整个 Section .disabled(true) iOS 13

💻 代码示例

import SwiftUI

struct FormView: View {
    @State private var username = ""
    @State private var email = ""
    @State private var birthDate = Date()
    @State private var notificationsEnabled = true
    @State private var selectedGender = 0
    @State private var sliderValue: Double = 50
    @State private var isFormValid = false

    let genders = ["男", "女", "其他"]

    var body: some View {
        Form {
            // 个人信息 Section
            Section("个人信息") {
                TextField("用户名", text: $username)
                TextField("邮箱", text: $email)
                    .keyboardType(.emailAddress)
                DatePicker("生日", selection: $birthDate, displayedComponents: .date)
                Picker("性别", selection: $selectedGender) {
                    ForEach(0..<genders.count, id: \.self) {
                        Text(genders[$0])
                    }
                }
            }

            // 设置 Section
            Section("设置") {
                Toggle("推送通知", isOn: $notificationsEnabled)

                Picker("主题颜色", selection: $selectedGender) {
                    Text("蓝色").tag(0)
                    Text("红色").tag(1)
                    Text("绿色").tag(2)
                }
                .pickerStyle(.menu)

                VStack(alignment: .leading) {
                    Text("音量: \(Int(sliderValue))%")
                    Slider(value: $sliderValue, in: 0...100)
                }
            }

            // 按钮 Section
            Section {
                Button("保存") {
                    // 保存逻辑
                    print("保存")
                }
                .disabled(username.isEmpty || email.isEmpty)

                Button("重置") {
                    // 重置逻辑
                    username = ""
                    email = ""
                }
                .foregroundColor(.red)
            }
        }
        .navigationTitle("设置")
        .onChange(of: username) { newValue in
            isFormValid = !username.isEmpty && !email.isEmpty
        }
        .onChange(of: email) { newValue in
            isFormValid = !username.isEmpty && !email.isEmpty
        }
    }
}

// 带验证的登录表单
struct LoginFormView: View {
    @State private var username = ""
    @State private var password = ""
    @State private var confirmPassword = ""
    @State private var showError = false
    @State private var errorMessage = ""

    var body: some View {
        Form {
            Section(header: Text("账户信息")) {
                TextField("用户名", text: $username)
                    .autocapitalization(.none)

                SecureField("密码", text: $password)

                SecureField("确认密码", text: $confirmPassword)
            }

            Section {
                Button("注册") {
                    if validateForm() {
                        // 注册逻辑
                    }
                }
                .disabled(username.isEmpty || password.isEmpty || confirmPassword.isEmpty)
            }
        }
        .navigationTitle("注册")
        .alert("错误", isPresented: $showError) {
            Button("确定") {}
        } message: {
            Text(errorMessage)
        }
    }

    func validateForm() -> Bool {
        if username.count < 3 {
            errorMessage = "用户名至少3个字符"
            showError = true
            return false
        }
        if password.count < 6 {
            errorMessage = "密码至少6个字符"
            showError = true
            return false
        }
        if password != confirmPassword {
            errorMessage = "两次密码不一致"
            showError = true
            return false
        }
        return true
    }
}

// 只读样式的 Form
struct ReadOnlyFormView: View {
    var body: some View {
        Form {
            Section("关于") {
                HStack {
                    Text("版本")
                    Spacer()
                    Text("1.0.0")
                        .foregroundColor(.secondary)
                }

                HStack {
                    Text("作者")
                    Spacer()
                    Text("SwiftUI学习者")
                        .foregroundColor(.secondary)
                }
            }

            Section("链接") {
                Link("官方网站", destination: URL(string: "https://apple.com")!)
                Link("隐私政策", destination: URL(string: "https://apple.com")!)
                Link("服务条款", destination: URL(string: "https://apple.com")!)
            }
        }
        .navigationTitle("关于")
    }
}

⚠️ 注意事项

问题 说明 解决方案
Form 中 Button 不响应 Form 需要导航包裹 使用 NavigationView 包裹
Section 样式不生效 iOS 版本差异 不同 iOS 版本 Section 样式不同

🔙 返回顶部


20. 🔍 SearchBar 搜索栏

难度:★★☆ 阅读时间:约6分钟

SearchBar 用于实现搜索功能,iOS 16+ 推荐使用 searchable。


📌 常用 API

API 说明 示例 iOS版本
.searchable(_:) 添加搜索栏 .searchable(text: $searchText) iOS 15
.searchSuggestions(_:) 搜索建议 .searchSuggestions { } iOS 16
.searchScopes(_:) 搜索范围 .searchScopes($scope) iOS 16
.onSubmit(of:_:) 搜索提交 .onSubmit(of: .search) { } iOS 16

💻 代码示例

import SwiftUI

struct SearchBarView: View {
    @State private var searchText = ""
    @State private var users = [
        "张三", "李四", "王五", "赵六", "钱七",
        "Alice", "Bob", "Charlie", "David", "Emma"
    ]

    var filteredUsers: [String] {
        if searchText.isEmpty {
            return users
        }
        return users.filter { $0.localizedCaseInsensitiveContains(searchText) }
    }

    var body: some View {
        NavigationView {
            List(filteredUsers, id: \.self) { user in
                Text(user)
            }
            .navigationTitle("搜索")
            .searchable(text: $searchText, prompt: "搜索用户")
        }
    }
}

// 带搜索建议的搜索 (iOS 16+)
struct AdvancedSearchView: View {
    @State private var searchText = ""
    @State private var suggestedCategories = ["首页", "发现", "消息", "我的"]
    @State private var hasSearched = false

    var body: some View {
        NavigationView {
            VStack {
                if hasSearched {
                    Text("搜索结果: \(searchText)")
                } else {
                    Text("请输入搜索内容")
                }
            }
            .navigationTitle("搜索")
            .searchable(text: $searchText) {
                // 搜索建议
                ForEach(suggestedCategories, id: \.self) { category in
                    Button {
                        searchText = category
                        hasSearched = true
                    } label: {
                        HStack {
                            Image(systemName: "magnifyingglass")
                            Text(category)
                        }
                    }
                }
            }
            .onSubmit(of: .search) {
                hasSearched = true
            }
        }
    }
}

// 带搜索范围的搜索 (iOS 16+)
struct ScopedSearchView: View {
    @State private var searchText = ""
    @State private var scope: SearchScope = .all
    @State private var products = [
        Product(name: "iPhone", type: .phone),
        Product(name: "iPad", type: .tablet),
        Product(name: "MacBook", type: .computer),
        Product(name: "AirPods", type: .accessory),
        Product(name: "Apple Watch", type: .wearable)
    ]

    enum SearchScope: String {
        case all = "全部"
        case phone = "手机"
        case tablet = "平板"
        case computer = "电脑"
        case accessory = "配件"
    }

    struct Product: Identifiable {
        let id = UUID()
        let name: String
        let type: SearchScope
    }

    var filteredProducts: [Product] {
        products.filter { product in
            let matchesScope = scope == .all || product.type == scope
            let matchesSearch = searchText.isEmpty || product.name.localizedCaseInsensitiveContains(searchText)
            return matchesScope && matchesSearch
        }
    }

    var body: some View {
        NavigationView {
            List(filteredProducts) { product in
                HStack {
                    Text(product.name)
                    Spacer()
                    Text(product.type.rawValue)
                        .foregroundColor(.secondary)
                }
            }
            .navigationTitle("产品")
            .searchable(text: $searchText)
            .searchScopes($scope) {
                ForEach(SearchScope.allCases, id: \.self) { scope in
                    Text(scope.rawValue)
                }
            }
        }
    }
}

⚠️ 注意事项

问题 说明 解决方案
searchable 不显示 iOS 版本过低 searchable 需要 iOS 15+
搜索建议不工作 iOS 版本差异 某些功能需要 iOS 16+

🔙 返回顶部


21. 🗺️ MapView 地图

难度:★★★ 阅读时间:约10分钟

MapView 用于显示地图和位置信息,需要导入 MapKit 框架。


📌 常用 API

API 说明 示例 iOS版本
Map(coordinateRegion:) 创建地图 Map(coordinateRegion: $region) iOS 14
MapAnnotation(_:) 添加标注 MapAnnotation(coordinate:) { } iOS 14
.userTrackingMode(_:) 用户位置追踪 .userTrackingMode(.follow) iOS 14

💻 代码示例

import SwiftUI
import MapKit

struct MapViewExample: View {
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074), // 北京
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )

    var body: some View {
        NavigationView {
            Map(coordinateRegion: $region)
                .navigationTitle("地图")
        }
    }
}

// 带标注的地图
struct MapWithAnnotationsView: View {
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074),
        span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
    )

    let locations = [
        Location(name: "天安门", coordinate: CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074)),
        Location(name: "故宫", coordinate: CLLocationCoordinate2D(latitude: 39.9163, longitude: 116.3972)),
        Location(name: "颐和园", coordinate: CLLocationCoordinate2D(latitude: 40.0022, longitude: 116.2733))
    ]

    var body: some View {
        NavigationView {
            Map(coordinateRegion: $region, annotationItems: locations) { location in
                MapMarker(coordinate: location.coordinate, tint: .red)
            }
            .navigationTitle("北京景点")
        }
    }
}

struct Location: Identifiable {
    let id = UUID()
    let name: String
    let coordinate: CLLocationCoordinate2D
}

// 自定义标注样式
struct CustomAnnotationView: View {
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074),
        span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
    )

    let locations = [
        Location(name: "天安门", coordinate: CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074)),
        Location(name: "故宫", coordinate: CLLocationCoordinate2D(latitude: 39.9163, longitude: 116.3972))
    ]

    var body: some View {
        Map(coordinateRegion: $region) {
            ForEach(locations) { location in
                MapAnnotation(coordinate: location.coordinate) {
                    VStack {
                        Text(location.name)
                            .font(.caption)
                            .padding(4)
                            .background(Color.white)
                            .cornerRadius(4)

                        Image(systemName: "mappin.circle.fill")
                            .font(.title)
                            .foregroundColor(.red)
                    }
                }
            }
        }
    }
}

⚠️ 注意事项

问题 说明 解决方案
Map 不显示 权限未配置 需要在 Info.plist 添加位置权限描述
模拟器无效果 模拟器地图有限 使用真机测试

Info.plist 配置

<key>NSLocationWhenInUseUsageDescription</key>
<string>需要访问您的位置以显示在地图上</string>

🔙 返回顶部


22. 💾 数据持久化

难度:★★★ 阅读时间:约15分钟

数据持久化是应用保存数据的关键技术。

📊 持久化方案对比

方案 适用场景 难度 iOS版本
UserDefaults 简单配置、用户设置 简单 iOS 13
SwiftData 结构化数据、关系型 中等 iOS 17
Core Data 复杂数据、历史项目 较难 iOS 13
FileManager 文件存储(JSON/ plist) 中等 iOS 13

22.1 UserDefaults 简单数据存储

适用于存储用户设置、配置等简单数据。

💻 代码示例

import SwiftUI

// UserDefaults 封装
class AppSettings: ObservableObject {
    @Published var isDarkMode: Bool {
        didSet {
            UserDefaults.standard.set(isDarkMode, forKey: "isDarkMode")
        }
    }

    @Published var userName: String {
        didSet {
            UserDefaults.standard.set(userName, forKey: "userName")
        }
    }

    init() {
        self.isDarkMode = UserDefaults.standard.bool(forKey: "isDarkMode")
        self.userName = UserDefaults.standard.string(forKey: "userName") ?? ""
    }
}

struct UserDefaultsView: View {
    @StateObject private var settings = AppSettings()

    var body: some View {
        Form {
            Section("设置") {
                Toggle("深色模式", isOn: $settings.isDarkMode)
                TextField("用户名", text: $settings.userName)
            }

            Section {
                Button("清除所有数据") {
                    resetDefaults()
                }
                .foregroundColor(.red)
            }
        }
        .navigationTitle("UserDefaults")
    }

    func resetDefaults() {
        let defaults = UserDefaults.standard
        let dictionary = defaults.dictionaryRepresentation()
        dictionary.keys.forEach { key in
            defaults.removeObject(forKey: key)
        }
        settings.isDarkMode = false
        settings.userName = ""
    }
}

22.2 SwiftData 数据库(iOS 17+)

Apple 最新的数据持久化框架,基于 Swift 宏。

💻 代码示例

import SwiftData
import SwiftUI

// 1. 定义模型
@Model
final class TodoItem {
    var title: String
    var isCompleted: Bool
    var createdAt: Date

    init(title: String, isCompleted: Bool = false) {
        self.title = title
        self.isCompleted = isCompleted
        self.createdAt = Date()
    }
}

// 2. 创建 ModelContainer
@main
struct TodoApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([TodoItem.self])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: TodoItem.self, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

// 3. 使用 SwiftData
struct TodoListView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [TodoItem]
    @State private var newItemTitle = ""

    init() {
        // 排序:未完成的在前
        let sortDescriptor = SortDescriptor(\TodoItem.createdAt, order: .reverse)
        _items = Query(filter: #Predicate { $0.isCompleted == false }, sort: [sortDescriptor])
    }

    var body: some View {
        NavigationStack {
            List {
                ForEach(items) { item in
                    HStack {
                        Button {
                            item.isCompleted.toggle()
                        } label: {
                            Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
                                .foregroundColor(item.isCompleted ? .green : .gray)
                        }

                        Text(item.title)
                            .strikethrough(item.isCompleted)

                        Spacer()

                        Text(item.createdAt.formatted(date: .abbreviated, time: .shortened))
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                }
                .delete {
                    indexSet in
                    for index in indexSet {
                        modelContext.delete(items[index])
                    }
                }
            }
            .navigationTitle("待办事项")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("添加", action: addItem)
                }
            }
        }
    }

    func addItem() {
        withAnimation {
            let newItem = TodoItem(title: "新事项 \(items.count + 1)")
            modelContext.insert(newItem)
        }
    }
}

// 添加新项目的视图
struct AddTodoView: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.dismiss) private var dismiss

    @State private var title = ""
    @State private var description = ""

    var body: some View {
        NavigationStack {
            Form {
                TextField("标题", text: $title)
                TextField("描述", text: $description)
            }
            .navigationTitle("新待办")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("取消") { dismiss() }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("保存") {
                        saveItem()
                    }
                    .disabled(title.isEmpty)
                }
            }
        }
    }

    func saveItem() {
        let newItem = TodoItem(title: title)
        modelContext.insert(newItem)
        dismiss()
    }
}

22.3 FileManager 文件存储

适用于存储 JSON、图片等文件。

💻 代码示例

import SwiftUI

// 待办事项模型
struct Todo: Codable, Identifiable {
    let id: UUID
    var title: String
    var isCompleted: Bool

    init(title: String, isCompleted: Bool = false) {
        self.id = UUID()
        self.title = title
        self.isCompleted = isCompleted
    }
}

// FileManager 文档目录操作
class FileManagerViewModel: ObservableObject {
    @Published var todos: [Todo] = []

    private let fileName = "todos.json"

    private var fileURL: URL {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            .appendingPathComponent(fileName)
    }

    init() {
        load()
    }

    // 保存数据
    func save() {
        do {
            let data = try JSONEncoder().encode(todos)
            try data.write(to: fileURL)
        } catch {
            print("保存失败: \(error)")
        }
    }

    // 加载数据
    func load() {
        do {
            let data = try Data(contentsOf: fileURL)
            todos = try JSONDecoder().decode([Todo].self, from: data)
        } catch {
            print("加载失败: \(error)")
            todos = []
        }
    }

    // 添加待办
    func addTodo(title: String) {
        let newTodo = Todo(title: title)
        todos.append(newTodo)
        save()
    }

    // 删除待办
    func deleteTodo(at indexSet: IndexSet) {
        todos.remove(atOffsets: indexSet)
        save()
    }

    // 切换完成状态
    func toggleCompletion(of todo: Todo) {
        if let index = todos.firstIndex(where: { $0.id == todo.id }) {
            todos[index].isCompleted.toggle()
            save()
        }
    }
}

struct FileManagerView: View {
    @StateObject private var viewModel = FileManagerViewModel()
    @State private var newItemTitle = ""

    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.todos) { todo in
                    HStack {
                        Button {
                            viewModel.toggleCompletion(of: todo)
                        } label: {
                            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                                .foregroundColor(todo.isCompleted ? .green : .gray)
                        }
                        Text(todo.title)
                            .strikethrough(todo.isCompleted)
                    }
                }
                .delete {
                    viewModel.deleteTodo(at: $0)
                }
            }
            .navigationTitle("文件存储")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("添加") {
                        showingAddItem = true
                    }
                }
            }
            .sheet(isPresented: $showingAddItem) {
                TextField("待办事项", text: $newItemTitle)
                    .textFieldStyle(.roundedBorder)
                    .padding()
                }
        }
    }
}

🔙 返回顶部


23. 🌐 网络请求

难度:★★★ 阅读时间:约12分钟

📊 网络方案对比

方案 说明 难度
URLSession Apple 原生,适合简单请求 中等
Async/Await 现代异步语法,推荐 简单
Alamofire 第三方库,功能丰富 简单

23.1 URLSession + Async/Await(推荐)

使用现代 Swift 并发语法进行网络请求。

💻 代码示例

import SwiftUI

// 数据模型
struct User: Codable, Identifiable {
    let id: Int
    let name: String
    let email: String
    let phone: String?
}

// API 错误
enum APIError: Error, LocalizedError {
    case invalidURL
    case networkError
    case decodingError
    case serverError(statusCode: Int)

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "无效的 URL"
        case .networkError:
            return "网络连接失败"
        case .decodingError:
            return "数据解析失败"
        case .serverError(let code):
            return "服务器错误 (\(code))"
        }
    }
}

// 网络管理器
@MainActor
class NetworkManager: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    private let baseURL = "https://jsonplaceholder.typicode.com"

    // GET 请求
    func fetchUsers() async {
        isLoading = true
        errorMessage = nil

        guard let url = URL(string: "\(baseURL)/users") else {
            errorMessage = APIError.invalidURL.errorDescription
            isLoading = false
            return
        }

        do {
            let (data, response) = try await URLSession.shared.data(from: url)

            guard let httpResponse = response as? HTTPURLResponse else {
                errorMessage = APIError.networkError.errorDescription
                isLoading = false
                return
            }

            guard (200...299).contains(httpResponse.statusCode) else {
                errorMessage = "服务器错误: \(httpResponse.statusCode)"
                isLoading = false
                return
            }

            let decodedUsers = try JSONDecoder().decode([User].self, from: data)
            users = decodedUsers

        } catch {
            self.errorMessage = error.localizedDescription
        }

        isLoading = false
    }

    // POST 请求
    func createUser(name: String, email: String) async -> Bool {
        isLoading = true

        guard let url = URL(string: "\(baseURL)/users") else {
            errorMessage = APIError.invalidURL.errorDescription
            isLoading = false
            return false
        }

        let newUser = User(id: 0, name: name, email: email, phone: nil)

        do {
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try JSONEncoder().encode(newUser)

            let (data, response) = try await URLSession.shared.data(for: request)

            if let httpResponse = response as? HTTPURLResponse,
               (200...299).contains(httpResponse.statusCode) {
                let createdUser = try JSONDecoder().decode(User.self, from: data)
                users.append(createdUser)
                isLoading = false
                return true
            }

        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false
        return false
    }
}

// 使用示例
struct NetworkRequestView: View {
    @StateObject private var networkManager = NetworkManager()

    var body: some View {
        NavigationView {
            Group {
                if networkManager.isLoading {
                    ProgressView("加载中...")
                } else if let error = networkManager.errorMessage {
                    VStack(spacing: 20) {
                        Image(systemName: "exclamationmark.triangle")
                            .font(.largeTitle)
                            .foregroundColor(.orange)
                        Text(error)
                            .foregroundColor(.red)
                        Button("重试") {
                            Task {
                                await networkManager.fetchUsers()
                            }
                        }
                    }
                } else if networkManager.users.isEmpty {
                    ContentUnavailableView {
                        Label("无数据", systemImage: "tray")
                    } description: {
                        Text("点击下方按钮获取数据")
                    } actions: {
                        Button("获取用户列表") {
                            Task {
                                await networkManager.fetchUsers()
                            }
                        }
                        .buttonStyle(.borderedProminent)
                    }
                } else {
                    List(networkManager.users) { user in
                        VStack(alignment: .leading, spacing: 5) {
                            Text(user.name)
                                .font(.headline)
                            Text(user.email)
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }
                    }
                }
            }
            .navigationTitle("网络请求")
            .refreshable {
                await networkManager.fetchUsers()
            }
            .task {
                // 视图出现时自动加载数据
                if networkManager.users.isEmpty {
                    await networkManager.fetchUsers()
                }
            }
        }
    }
}

// 加载图片示例
class ImageLoader: ObservableObject {
    @Published var image: UIImage?
    @Published var isLoading = false

    func loadImage(from urlString: String) async {
        isLoading = true

        guard let url = URL(string: urlString) else {
            isLoading = false
            return
        }

        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            if let uiImage = UIImage(data: data) {
                image = uiImage
            }
        } catch {
            print("图片加载失败: \(error)")
        }

        isLoading = false
    }
}

struct AsyncImageView: View {
    @StateObject private var loader = ImageLoader()
    let urlString: String

    var body: some View {
        Group {
            if loader.isLoading {
                ProgressView()
                    .frame(width: 100, height: 100)
            } else if let image = loader.image {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else {
                Image(systemName: "photo")
                    .foregroundColor(.gray)
            }
        }
        .frame(width: 100, height: 100)
        .onAppear {
            Task {
                await loader.loadImage(from: urlString)
            }
        }
    }
}

23.2 网络请求最佳实践

⚡ 性能优化

// 图片缓存
class ImageCache {
    static let shared = ImageCache()
    private let cache = NSCache<NSString, UIImage>()

    func get(forKey key: String) -> UIImage? {
        cache.object(forKey: key as NSString)
    }

    func set(_ image: UIImage, forKey key: String) {
        cache.setObject(image, forKey: key as NSString)
    }
}

// 请求重试
func fetchWithRetry<T: Decodable>(
    url: URL,
    retryCount: Int = 3
) async throws -> T {
    var lastError: Error?

    for _ in 0..<retryCount {
        do {
            let (data, response) = try await URLSession.shared.data(from: url)

            if let httpResponse = response as? HTTPURLResponse {
                if (200...299).contains(httpResponse.statusCode) {
                    return try JSONDecoder().decode(T.self, from: data)
                }
            }
        } catch {
            lastError = error
            // 等待后重试
            try await Task.sleep(nanoseconds: UInt64(1_000_000_000))
        }
    }

    throw lastError ?? APIError.networkError
}

// 并发请求
func fetchMultipleData() async {
    async let users = fetchUsers()
    async let posts = fetchPosts()
    async let comments = fetchComments()

    let (fetchedUsers, fetchedPosts, fetchedComments) = await (users, posts, comments)

    // 处理结果
}

func fetchUsers() async -> [User] { [] }
func fetchPosts() async -> [Post] { [] }
func fetchComments() async -> [Comment] { [] }
struct Post: Decodable, Identifiable {}
struct Comment: Decodable, Identifiable {}

🔒 安全性

// HTTPS 证书校验(生产环境)
class SecureSessionDelegate: NSObject, URLSessionDelegate {
    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        // 开发环境可跳过,生产环境必须校验
        #if DEBUG
        completionHandler(.useCredential, nil)
        #else
        // 生产环境证书校验逻辑
        completionHandler(.performDefaultHandling, nil)
        #endif
    }
}

⚠️ 注意事项

问题 说明 解决方案
App Transport Security iOS 默认只允许 HTTPS 在 Info.plist 配置 NSAppTransportSecurity
主线程更新 UI 网络请求在后台线程 使用 @MainActorDispatchQueue.main.async
JSON 解码失败 模型不匹配 检查 CodingKeys,确保字段名一致

🔙 返回顶部


24. 🏗️ MVVM 架构

难度:★★★ 阅读时间:约20分钟

MVVM(Model-View-ViewModel)是 SwiftUI 推荐的架构模式。

📐 MVVM 架构图

┌─────────────┐
│    View     │ ← SwiftUI 视图
└──────┬──────┘
       │ 观察/绑定
┌──────▼──────┐
│  ViewModel  │ ← 业务逻辑、状态管理
└──────┬──────┘
       │ 数据操作
┌──────▼──────┐
│    Model    │ ← 数据模型
└─────────────┘

📋 各层职责

职责
Model 数据模型、业务实体
View UI 界面、用户交互
ViewModel 状态管理、业务逻辑、数据处理

💻 完整 MVVM 示例

1. Model 层

import Foundation

// 待办事项模型
struct TodoItem: Identifiable, Codable {
    let id: UUID
    var title: String
    var isCompleted: Bool
    var createdAt: Date

    init(title: String, isCompleted: Bool = false) {
        self.id = UUID()
        self.title = title
        self.isCompleted = isCompleted
        self.createdAt = Date()
    }
}

2. ViewModel 层

import SwiftUI

// ViewModel 处理业务逻辑
@MainActor
class TodoViewModel: ObservableObject {
    // 输出:发布状态供 View 观察
    @Published var todos: [TodoItem] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    // 依赖注入
    private let repository: TodoRepository

    init(repository: TodoRepository = TodoRepository()) {
        self.repository = repository
    }

    // 输入:View 调用的方法
    func loadTodos() {
        isLoading = true

        Task {
            do {
                todos = try await repository.fetchTodos()
            } catch {
                errorMessage = error.localizedDescription
            }
            isLoading = false
        }
    }

    func addTodo(title: String) {
        let newTodo = TodoItem(title: title)
        todos.append(newTodo)

        Task {
            try? await repository.save(todos)
        }
    }

    func deleteTodo(at indexSet: IndexSet) {
        todos.remove(atOffsets: indexSet)

        Task {
            try? await repository.save(todos)
        }
    }

    func toggleCompletion(of todo: TodoItem) {
        if let index = todos.firstIndex(where: { $0.id == todo.id }) {
            todos[index].isCompleted.toggle()
        }
    }
}

3. Repository 层(数据层)

// 数据仓库:统一管理数据来源
struct TodoRepository {
    private let fileName = "todos.json"

    private var fileURL: URL {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            .appendingPathComponent(fileName)
    }

    func fetchTodos() async throws -> [TodoItem] {
        // 从本地加载
        let data = try Data(contentsOf: fileURL)
        return try JSONDecoder().decode([TodoItem].self, from: data)
    }

    func save(_ todos: [TodoItem]) async throws {
        let data = try JSONEncoder().encode(todos)
        try data.write(to: fileURL)
    }
}

4. View 层

// 主视图
struct TodoListView: View {
    // 连接 ViewModel
    @StateObject private var viewModel = TodoViewModel()
    @State private var showingAddSheet = false

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView("加载中...")
                } else if viewModel.todos.isEmpty {
                    ContentUnavailableView {
                        Label("暂无待办事项", systemImage: "tray")
                    }
                } else {
                    List {
                        ForEach(viewModel.todos) { todo in
                            TodoRowView(todo: todo) {
                                viewModel.toggleCompletion(of: todo)
                            }
                        }
                        .delete {
                            viewModel.deleteTodo(at: $0)
                        }
                    }
                }
            }
            .navigationTitle("待办事项")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("添加", systemImage: .plus) {
                        showingAddSheet = true
                    }
                }
            }
            .sheet(isPresented: $showingAddSheet) {
                AddTodoView { title in
                    viewModel.addTodo(title: title)
                }
            }
            .task {
                await viewModel.loadTodos()
            }
        }
    }
}

// 行视图
struct TodoRowView: View {
    let todo: TodoItem
    let onTap: () -> Void

    var body: some View {
        HStack(spacing: 12) {
            Button(action: onTap) {
                Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title2)
                    .foregroundColor(todo.isCompleted ? .green : .gray)
            }

            VStack(alignment: .leading, spacing: 4) {
                Text(todo.title)
                    .strikethrough(todo.isCompleted)
                Text(todo.createdAt.formatted(date: .abbreviated, time: .omitted))
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
        .padding(.vertical, 4)
    }
}

// 添加视图
struct AddTodoView: View {
    @Environment(\.dismiss) private var dismiss
    @State private var title = ""

    let onSave: (String) -> Void

    var body: some View {
        NavigationStack {
            Form {
                TextField("待办事项", text: $title)
            }
            .navigationTitle("新建待办")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("取消") { dismiss() }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("保存") {
                        onSave(title)
                        dismiss()
                    }
                    .disabled(title.isEmpty)
                }
            }
        }
    }
}

🎯 MVVM 最佳实践

实践 说明
单一职责 ViewModel 只负责业务逻辑,不包含 UI 代码
依赖注入 通过 init 注入 Repository,方便测试
@MainActor 确保 ViewModel 在主线程更新 UI
Error Handling 统一错误处理,通过 @Published 发布
测试友好 ViewModel 与 View 解耦,可单独测试

📦 完整项目结构

MyTodoApp/
├── Models/
│   └── TodoItem.swift
├── ViewModels/
│   └── TodoViewModel.swift
├── Views/
│   ├── TodoListView.swift
│   ├── TodoRowView.swift
│   └── AddTodoView.swift
├── Repositories/
│   └── TodoRepository.swift
├── Services/
│   └── NetworkService.swift
├── Resources/
│   └── Assets.xcassets
├── App/
│   └── MyApp.swift
└── Info.plist

🔙 返回顶部


25. 📦 常用第三方库

难度:★★☆ 阅读时间:约10分钟

SwiftUI 生态中有许多优秀的第三方库,可以帮助你快速实现复杂功能。

💡 安装方式:在 Xcode 中通过 Swift Package Manager (SPM) 安装

  1. File → Add Package Dependencies
  2. 输入库的 GitHub URL
  3. 选择版本并添加到项目

25.1 🖼️ SDWebImageSwiftUI - 异步图片加载

GitHub: SDWebImage/SDWebImageSwiftUI

用于异步加载和缓存网络图片,支持 GIF 和 WebP 格式。

📌 常用 API

API 说明
WebImage(url:) 从网络加载图片
.resizable() 使图片可调整大小
.indicator(_:) 设置加载指示器

💻 代码示例

import SDWebImageSwiftUI
import SwiftUI

struct WebImageView: View {
    let imageURL = URL(string: "https://example.com/image.jpg")

    var body: some View {
        WebImage(url: imageURL)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 200, height: 200)
    }
}

14.2 🖼️ Kingfisher - 图片加载与缓存

GitHub: onevcat/Kingfisher

另一个强大的图片加载库,API 更简洁。

💻 代码示例

import Kingfisher
import SwiftUI

struct KingfisherView: View {
    let imageURL = URL(string: "https://example.com/image.jpg")

    var body: some View {
        KFImage(imageURL)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 200, height: 200)
    }
}

14.3 🎬 Lottie - 动画库

GitHub: airbnb/lottie-swift

After Effects 动画的完美解决方案,支持 JSON 格式的动画文件。

💻 代码示例

import Lottie
import SwiftUI

struct LottieAnimationView: View {
    @State private var isPlaying = true

    var body: some View {
        LottieView(animation: .named("loading"))
            .playing(isPlaying)
            .looping()
            .frame(width: 200, height: 200)
    }
}

14.4 🌐 Alamofire - 网络请求

GitHub: Alamofire/Alamofire

Swift 最流行的 HTTP 网络库。

💻 代码示例

import Alamofire
import SwiftUI

struct NetworkView: View {
    @State private var users: [User] = []

    var body: some View {
        List(users, id: \.id) { user in
            Text(user.name)
        }
        .onAppear {
            fetchUsers()
        }
    }

    func fetchUsers() {
        AF.request("https://jsonplaceholder.typicode.com/users")
            .validate()
            .responseDecodable(of: [User].self) { response in
                switch response.result {
                case .success(let users):
                    self.users = users
                case .failure(let error):
                    print(error)
                }
            }
    }
}

📊 第三方库快速对比

库名 用途 推荐指数 难度
SDWebImageSwiftUI 图片加载 ⭐⭐⭐⭐⭐ 简单
Kingfisher 图片加载 ⭐⭐⭐⭐⭐ 简单
Lottie 动画 ⭐⭐⭐⭐ 中等
Alamofire 网络请求 ⭐⭐⭐⭐⭐ 中等

🔙 返回顶部


🎨 颜色参考表

难度:★☆☆ 阅读时间:约3分钟

SwiftUI 提供了丰富的内置颜色。

🌈 预设颜色

颜色 代码 示例
黑色 .black ⚫️
白色 .white ⚪️
灰色 .gray ⚪️
红色 .red 🔴
橙色 .orange 🟠
黄色 .yellow 🟡
绿色 .green 🟢
蓝色 .blue 🔵
紫色 .purple 🟣
粉色 .pink 🩷
青色 .cyan 🔷

🔧 自定义颜色

// 使用RGB
Color(red: 255/255, green: 100/255, blue: 50/255)

// 使用十六进制 (需要扩展)
Color(hex: "#FF6432")

// 使用UIColor
Color(UIColor.systemBlue)

🎭 语义颜色

颜色 说明 iOS版本
.primary 主要文本颜色 iOS 13
.secondary 次要文本颜色 iOS 13
.accentColor 强调色 iOS 13

🔙 返回顶部


✨ SF Symbols 图标速查

难度:★☆☆ 阅读时间:约5分钟

SF Symbols 是 Apple 提供的系统图标库,包含 5000+ 图标。

📱 常用图标

图标 systemName 说明
🔙 chevron.left 左箭头
🔜 chevron.right 右箭头
🔼 chevron.up 上箭头
🔽 chevron.down 下箭头
plus 加号
minus 减号
✏️ pencil 铅笔
🗑️ trash 垃圾桶
star 星星(空心)
star.fill 星星(实心)
❤️ heart 心形(空心)
❤️ heart.fill 心形(实心)
👁️ eye 眼睛
🔒 lock
🔓 lock.open 开锁
📱 iphone iPhone
💻 laptopcomputer 电脑
⚙️ gearshape 设置
🏠 house 房子
📁 folder 文件夹
📄 doc 文档
🔍 magnifyingglass 搜索
🔔 bell 铃铛

💡 使用技巧

// 空心和实心图标
Image(systemName: "heart")      // 空心
Image(systemName: "heart.fill") // 实心

// 不同尺寸
Image(systemName: "star.fill")
    .font(.system(size: 20))      // 小
    .font(.system(size: 50))      // 大
    .imageScale(.large)           // 特大

// 可变颜色
Image(systemName: "star.fill")
    .symbolRenderingMode(.hierarchical) // 分层渲染
    .foregroundColor(.blue)

📚 查看更多:下载 SF Symbols App 浏览所有图标

🔙 返回顶部


🛠️ 通用修饰符

难度:★★☆ 阅读时间:约8分钟

以下修饰符适用于大多数SwiftUI视图:

修饰符 说明 示例 iOS版本
.padding() 添加内边距 .padding().padding(20) iOS 13
.frame(_:) 设置尺寸 .frame(width: 100, height: 100) iOS 13
.background(_:) 设置背景 .background(Color.blue) iOS 13
.cornerRadius(_:) 设置圆角 .cornerRadius(10) iOS 13
.shadow(radius:) 设置阴影 .shadow(radius: 5) iOS 13
.opacity(_:) 设置透明度 .opacity(0.5) iOS 13
.overlay(_:) 添加覆盖层 .overlay(Text("覆盖")) iOS 13
.rotationEffect(_:) 旋转视图 .rotationEffect(.degrees(45)) iOS 13
.scaleEffect(_:) 缩放视图 .scaleEffect(1.5) iOS 13
.offset(_:) 偏移视图 .offset(x: 10, y: 20) iOS 13
.animation(_:) 添加动画 .animation(.default) iOS 13
.transition(_:) 过渡效果 .transition(.slide) iOS 13

🔙 返回顶部


⚡ 性能优化建议

难度:★★★ 阅读时间:约10分钟

建议 说明 示例
使用 LazyVStack/LazyHStack 懒加载,只渲染可见视图 替换大量的VStack/HStack
使用 LazyVGrid/LazyHGrid 网格懒加载 替换大量Grid
避免深层嵌套 减少视图层级 使用 .background() 而非 ZStack
@State vs @StateObject 选择正确的状态管理 引用类型用@StateObject
使用 equatable() 减少不必要的重绘 Text("Hello").equatable()
避免在 body 中计算 将复杂计算移到外部 使用computed property
使用 .id() 修饰符 强制视图刷新 .id(refreshTrigger)

🔄 状态管理选择

类型 用途 iOS版本
@State 视图内部状态 iOS 13
@Binding 双向绑定子视图状态 iOS 13
@StateObject 拥有 ObservableObject iOS 14
@ObservedObject 观察 ObservableObject iOS 13
@EnvironmentObject 全局环境对象 iOS 13
@Published 发布属性变化 iOS 13

🔙 返回顶部


🐛 常见错误及解决方案

难度:★★☆ 阅读时间:约8分钟

错误/问题 原因 解决方案
Cannot convert value of type 'String' to expected argument type 'Binding<String>' 未使用 $ 绑定 TextField("占位符", text: $name)
Type 'XXX' cannot conform to 'View' 返回多个视图未包裹在容器中 使用 VStack/HStack/Group 包裹
Modifier could not be synthesized 修饰符顺序错误 调整修饰符顺序
'XXX' is only available in iOS 15.0 or newer iOS版本不支持 升级 deployment target 或使用旧 API
View's body should not contain complex logic body中包含复杂逻辑 将逻辑提取到computed property
List requires Identifiable item ForEach 缺少 id 添加 id: \.self 或实现 Identifiable
Cannot convert value of type 'Text' to closure result type 'some View' 条件语句未处理所有分支 确保if/else返回相同类型
Missing argument for parameter 'body' 自定义View缺少body 实现 var body: some View

🔧 调试技巧

// 1. 打印视图层级
// 使用 Xcode 的 View Debugging 功能

// 2. 检查状态变化
let _ = print("State changed: \(yourState)")

// 3. 使用 .onAppear 和 .onDisappear 追踪生命周期
Text("Hello")
    .onAppear { print("appeared") }
    .onDisappear { print("disappeared") }

🔙 返回顶部


💡 实战小项目

难度:★★★ 阅读时间:约15分钟

📱 登录页面

综合运用 TextField、Button、SecureField 等控件。

import SwiftUI

struct LoginView: View {
    @State private var username = ""
    @State private var password = ""
    @State private var showAlert = false
    @State private var alertMessage = ""

    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                // Logo
                Image(systemName: "person.circle.fill")
                    .font(.system(size: 80))
                    .foregroundColor(.blue)

                // 用户名输入
                TextField("用户名", text: $username)
                    .textFieldStyle(.roundedBorder)
                    .padding(.horizontal)

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

                // 登录按钮
                Button("登录") {
                    login()
                }
                .frame(maxWidth: .infinity)
                .padding()
                .background(username.isEmpty || password.isEmpty ? Color.gray : Color.blue)
                .foregroundColor(.white)
                .cornerRadius(10)
                .padding(.horizontal)
                .disabled(username.isEmpty || password.isEmpty)

                Spacer()
            }
            .navigationTitle("登录")
            .alert("提示", isPresented: $showAlert) {
                Button("确定") {}
            } message: {
                Text(alertMessage)
            }
        }
    }

    func login() {
        // 模拟登录验证
        if username.count < 3 {
            alertMessage = "用户名至少3个字符"
            showAlert = true
        } else {
            alertMessage = "登录成功!"
            showAlert = true
        }
    }
}

⚙️ 设置页面

综合使用 List、Toggle、Picker 等控件。

import SwiftUI

struct SettingsView: View {
    @State private var notificationsEnabled = true
    @State private var darkMode = false
    @State private var languageIndex = 0
    @State private var sliderValue: Double = 50

    let languages = ["简体中文", "English", "日本語"]

    var body: some View {
        NavigationView {
            Form {
                // 通知设置
                Section(header: Text("通知")) {
                    Toggle("推送通知", isOn: $notificationsEnabled)
                }

                // 外观设置
                Section(header: Text("外观")) {
                    Toggle("深色模式", isOn: $darkMode)
                    Picker("语言", selection: $languageIndex) {
                        ForEach(0..<languages.count, id: \.self) { index in
                            Text(languages[index]).tag(index)
                        }
                    }
                }

                // 其他设置
                Section(header: Text("其他")) {
                    VStack(alignment: .leading) {
                        Text("音量: \(Int(sliderValue))%")
                        Slider(value: $sliderValue, in: 0...100)
                    }
                }

                // 关于
                Section(header: Text("关于")) {
                    HStack {
                        Text("版本")
                        Spacer()
                        Text("1.0.0")
                            .foregroundColor(.secondary)
                    }
                }
            }
            .navigationTitle("设置")
        }
    }
}

🔙 返回顶部


📚 总结

以上就是SwiftUI中最常用的基础控件及其API。掌握这些控件后,你就可以开始构建各种iOS应用界面了。

🎯 学习建议

  1. 从简单开始 - 先掌握Text、Button、Image等基础控件
  2. 理解布局 - VStack、HStack、ZStack是布局的基础
  3. 状态管理 - 熟练使用@State来管理UI状态
  4. 多练习 - 尝试组合不同的控件创建复杂的界面
  5. 查看文档 - Apple官方文档有详细的控件说明

📋 快速参考表

控件 用途 核心属性 iOS版本
Text 显示文本 font, foregroundColor, lineLimit 13.0+
Image 显示图片 resizable, aspectRatio, clipShape 13.0+
Button 响应点击 buttonStyle, disabled 13.0+
TextField 接收文本输入 textFieldStyle, SecureField 13.0+
Toggle 开关控制 isOn绑定 13.0+
Slider 数值选择 value绑定, in范围, step步进 13.0+
Picker 选项选择 selection绑定, pickerStyle 13.0+
DatePicker 日期选择 selection绑定, displayedComponents 13.0+
VStack 垂直布局 spacing, alignment 13.0+
HStack 水平布局 spacing, alignment 13.0+
ZStack 层叠布局 alignment 13.0+
ScrollView 滚动视图 .horizontal, .vertical 13.0+
List 列表显示 Section, listStyle 13.0+
Animation 动画效果 .animation(), .transition(), withAnimation 13.0+
NavigationStack 页面导航 path, navigationDestination, navigationTitle 16.0+
Sheet 弹出视图 .sheet(), .presentationDetents() 13.0+
Alert 提示框 .alert(), .confirmationDialog() 13.0+
TabView 标签栏 selection, .tabItem(), .pageStyle 13.0+
Form 表单容器 Section, formStyle 13.0+
SearchBar 搜索栏 text, prompt, isPresenting 15.0+
MapView 地图显示 mapStyle, cameraPosition 17.0+
UserDefaults 简单数据存储 standard, set, get 13.0+
SwiftData 数据库框架 @Model, ModelContainer 17.0+
FileManager 文件管理 default, documentDirectory 13.0+
URLSession 网络请求 data, decode, Async/Await 13.0+
MVVM 架构模式 @StateObject, @ObservedObject, @Published 13.0+

📖 推荐资源


祝你在SwiftUI的学习之旅中愉快! 🚀
Made with ❤️ for SwiftUI Beginners
posted @ 2026-01-30 14:19  hooong  阅读(4)  评论(0)    收藏  举报