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小时 | 掌握数据存储和网络请求 |
| ⚪ 参考篇 | 按需查阅 | 作为开发手册随时查阅 |
📑 目录
🟢 基础控件 (入门篇)
- 📝 Text 文本控件 ★☆☆ 约5分钟
- 🖼️ Image 图片控件 ★☆☆ 约8分钟
- 🔘 Button 按钮 ★☆☆ 约6分钟
- ✍️ TextField 文本输入框 ★☆☆ 约7分钟
- 🔀 Toggle 开关 ★☆☆ 约4分钟
- 🎚️ Slider 滑块 ★☆☆ 约5分钟
- 🎯 Picker 选择器 ★★☆ 约8分钟
- 📅 DatePicker 日期选择器 ★★☆ 约6分钟
🟡 布局控件 (核心篇)
- ↕️ VStack 垂直布局 ★☆☆ 约5分钟
- ↔️ HStack 水平布局 ★☆☆ 约5分钟
- 📚 ZStack 层叠布局 ★☆☆ 约5分钟
- 📜 ScrollView 滚动视图 ★★☆ 约8分钟
- 📋 List 列表 ★★☆ 约10分钟
🔵 交互控件 (进阶篇)
- 🎬 Animation 动画 ★★★ 约12分钟
- 🧭 NavigationStack 导航 ★★☆ 约10分钟
- 📄 Sheet 弹窗 ★★☆ 约8分钟
- ⚠️ Alert 提示框 ★★☆ 约7分钟
🟣 高级控件 (应用篇)
- 🧩 TabView 标签栏 ★★☆ 约8分钟
- 📝 Form 表单 ★★☆ 约7分钟
- 🔍 SearchBar 搜索栏 ★★☆ 约6分钟
- 🗺️ MapView 地图 ★★★ 约10分钟
🟠 数据与网络 (实战篇)
- 💾 数据持久化 ★★★ 约15分钟
- 🌐 网络请求 ★★★ 约12分钟
- 🏗️ MVVM 架构 ★★★ 约20分钟
⚪ 扩展内容 (参考篇)
- 📦 常用第三方库 ★★☆ 约10分钟
- 🎨 颜色参考表 ★☆☆ 约3分钟
- ✨ SF Symbols 图标速查 ★☆☆ 约5分钟
- 🛠️ 通用修饰符 ★★☆ 约8分钟
- ⚡ 性能优化建议 ★★★ 约10分钟
- 🐛 常见错误及解决方案 ★★☆ 约8分钟
- 💡 实战小项目 ★★★ 约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 | 网络请求在后台线程 | 使用 @MainActor 或 DispatchQueue.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) 安装
- File → Add Package Dependencies
- 输入库的 GitHub URL
- 选择版本并添加到项目
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应用界面了。
🎯 学习建议
- 从简单开始 - 先掌握Text、Button、Image等基础控件
- 理解布局 - VStack、HStack、ZStack是布局的基础
- 状态管理 - 熟练使用@State来管理UI状态
- 多练习 - 尝试组合不同的控件创建复杂的界面
- 查看文档 - 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+ |

浙公网安备 33010602011771号