《SwiftUI 进阶第6章:列表与滚动视图》
6.1 List 组件详解
List 介绍
List 是 SwiftUI 中用于显示有序数据集合的强大组件,它自动处理滚动、单元格复用、分割线等功能。
基本用法
import SwiftUI
struct SimpleListView: View {
// 示例数据
let items = ["项目 1", "项目 2", "项目 3", "项目 4", "项目 5"]
var body: some View {
List {
ForEach(items, id: \.self) {
Text($0)
}
}
.navigationTitle("简单列表")
}
}
#Preview {
NavigationStack {
SimpleListView()
}
}
数据模型
对于更复杂的数据,建议创建符合 Identifiable 协议的数据模型:
// 符合 Identifiable 协议的数据模型
struct TodoItem: Identifiable {
let id = UUID()
var title: String
var isCompleted: Bool
}
struct TodoListView: View {
// 待办事项数据
let todos: [TodoItem] = [
TodoItem(title: "学习 SwiftUI", isCompleted: false),
TodoItem(title: "完成作业", isCompleted: true),
TodoItem(title: "购买 groceries", isCompleted: false)
]
var body: some View {
List(todos) { todo in
HStack {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(todo.isCompleted ? .green : .gray)
Text(todo.title)
.strikethrough(todo.isCompleted, color: .gray)
}
}
.navigationTitle("待办事项")
}
}
列表样式
SwiftUI 提供了多种列表样式:
List {
// 列表内容
}
// 不同的列表样式
.listStyle(.plain) // 简单样式
.listStyle(.grouped) // 分组样式
.listStyle(.insetGrouped) // 内嵌分组样式
.listStyle(.sidebar) // 侧边栏样式(macOS)
.listStyle(.automatic) // 自动适应平台
可编辑列表
struct EditableListView: View {
@State private var items = ["项目 1", "项目 2", "项目 3", "项目 4", "项目 5"]
var body: some View {
List {
ForEach(items, id: \.self) {
Text($0)
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItems)
}
.navigationTitle("可编辑列表")
.toolbar {
EditButton()
}
}
// 删除项目
private func deleteItems(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
}
// 移动项目
private func moveItems(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
}
}
6.2 Section 分组
Section 介绍
Section 用于将列表内容分组,每个分组可以有标题和页脚。
基本用法
struct SectionedListView: View {
let fruits = ["苹果", "香蕉", "橙子"]
let vegetables = ["胡萝卜", "土豆", "西红柿"]
var body: some View {
List {
Section("水果") {
ForEach(fruits, id: \.self) {
Text($0)
}
}
Section("蔬菜") {
ForEach(vegetables, id: \.self) {
Text($0)
}
}
}
.navigationTitle("分组列表")
.listStyle(.insetGrouped)
}
}
带页脚的分组
struct SectionWithFooter: View {
let popularMovies = ["电影 A", "电影 B", "电影 C"]
let recentMovies = ["电影 D", "电影 E"]
var body: some View {
List {
Section(
"热门电影",
footer: Text("这些是当前最受欢迎的电影")
) {
ForEach(popularMovies, id: \.self) {
Text($0)
}
}
Section(
"最近上映",
footer: Text("这些是最近上映的电影")
) {
ForEach(recentMovies, id: \.self) {
Text($0)
}
}
}
.navigationTitle("电影列表")
.listStyle(.insetGrouped)
}
}
动态分组
// 按首字母分组的联系人
struct Contact: Identifiable {
let id = UUID()
let name: String
let section: String // 用于分组的首字母
}
struct ContactListView: View {
// 模拟联系人数据
let contacts: [Contact] = [
Contact(name: "张三", section: "Z"),
Contact(name: "李四", section: "L"),
Contact(name: "王五", section: "W"),
Contact(name: "赵六", section: "Z")
]
// 按 section 分组
var groupedContacts: [String: [Contact]] {
Dictionary(grouping: contacts, by: \.section)
}
var body: some View {
List {
ForEach(groupedContacts.keys.sorted(), id: \.self) { section in
Section(section) {
ForEach(groupedContacts[section]!) {
Text($0.name)
}
}
}
}
.navigationTitle("联系人")
.listStyle(.insetGrouped)
}
}
6.3 ForEach 动态列表
ForEach 介绍
ForEach 用于根据数据动态生成视图,是创建动态列表的核心组件。
基本用法
// 使用数组索引
ForEach(0..<5) { index in
Text("项目 \(index + 1)")
}
// 使用 Identifiable 数据
ForEach(items) { item in
Text(item.name)
}
// 使用显式 ID
ForEach(items, id: \.self) {
Text($0)
}
复杂数据结构
struct Product: Identifiable {
let id = UUID()
let name: String
let price: Double
let isInStock: Bool
}
struct ProductListView: View {
let products: [Product] = [
Product(name: "iPhone", price: 5999, isInStock: true),
Product(name: "iPad", price: 3999, isInStock: false),
Product(name: "MacBook", price: 9999, isInStock: true)
]
var body: some View {
List {
ForEach(products) { product in
HStack {
VStack(alignment: .leading) {
Text(product.name)
.font(.headline)
Text("¥\(product.price)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Text(product.isInStock ? "有货" : "缺货")
.foregroundStyle(product.isInStock ? .green : .red)
.font(.caption)
.padding(4)
.background(product.isInStock ? Color.green.opacity(0.1) : Color.red.opacity(0.1))
.cornerRadius(4)
}
}
}
.navigationTitle("产品列表")
}
}
性能优化
当处理大量数据时,使用 ForEach 的性能优化技巧:
-
使用稳定的 ID:避免使用
UUID()作为临时 ID -
使用
LazyVStack:对于非常长的列表,考虑使用ScrollView+LazyVStack -
避免在
ForEach中进行复杂计算:将计算移到视图外部
6.4 ScrollView 滚动视图
ScrollView 介绍
ScrollView 是一个通用的滚动容器,可以包含任何视图内容,不局限于列表。
基本用法
struct SimpleScrollView: View {
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(1..<20) {
Text("项目 \($0)")
.font(.title)
.frame(width: 300, height: 100)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
}
.navigationTitle("简单滚动视图")
}
}
水平滚动
struct HorizontalScrollView: View {
let items = ["红色", "绿色", "蓝色", "黄色", "紫色", "橙色"]
let colors: [Color] = [.red, .green, .blue, .yellow, .purple, .orange]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(0..<items.count, id: \.self) {
VStack {
Rectangle()
.fill(colors[$0])
.frame(width: 150, height: 150)
.cornerRadius(8)
Text(items[$0])
.font(.headline)
}
}
}
.padding()
}
.navigationTitle("水平滚动")
}
}
双向滚动
struct BidirectionalScrollView: View {
var body: some View {
ScrollView([.horizontal, .vertical]) {
VStack(spacing: 20) {
ForEach(1..<10) { row in
HStack(spacing: 20) {
ForEach(1..<10) { col in
Text("\(row),\(col)")
.frame(width: 100, height: 100)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
.font(.headline)
}
}
}
}
.padding()
}
.navigationTitle("双向滚动")
}
}
刷新功能
struct RefreshableScrollView: View {
@State private var items = ["项目 1", "项目 2", "项目 3"]
@State private var isLoading = false
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(items, id: \.self) {
Text($0)
.font(.title)
.frame(maxWidth: .infinity, minHeight: 100)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
}
.refreshable {
// 模拟网络请求
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
items.append("项目 \(items.count + 1)")
isLoading = false
}
}
.navigationTitle("可刷新滚动视图")
}
}
6.5 懒加载容器:LazyVStack、LazyHStack
懒加载容器介绍
LazyVStack 和 LazyHStack 是懒加载的栈容器,只在需要时创建视图,非常适合处理大量数据。
LazyVStack 基本用法
struct LazyVStackExample: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 20) {
ForEach(1..<1000) {
Text("项目 \($0)")
.font(.title)
.frame(maxWidth: .infinity, minHeight: 100)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
}
.navigationTitle("LazyVStack 示例")
}
}
LazyHStack 基本用法
struct LazyHStackExample: View {
var body: some View {
ScrollView(.horizontal) {
LazyHStack(spacing: 20) {
ForEach(1..<100) {
VStack {
Text("项目 \($0)")
.font(.title)
.frame(width: 200, height: 200)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
}
.padding()
}
.navigationTitle("LazyHStack 示例")
}
}
性能对比
| 容器类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| VStack | 简单,适合少量内容 | 一次性创建所有视图 | 内容较少的垂直布局 |
| HStack | 简单,适合少量内容 | 一次性创建所有视图 | 内容较少的水平布局 |
| LazyVStack | 懒加载,性能好 | 语法稍复杂 | 大量内容的垂直列表 |
| LazyHStack | 懒加载,性能好 | 语法稍复杂 | 大量内容的水平列表 |
| List | 功能丰富,自带滚动 | 样式固定 | 标准列表界面 |
实战:创建一个产品展示列表
需求分析
创建一个产品展示列表,包含以下功能:
- 产品图片、名称、价格、描述
- 分组显示(热门产品、新品)
- 下拉刷新
- 加载更多
- 产品状态(有货/缺货)
代码实现
import SwiftUI
// 产品模型
struct Product: Identifiable {
let id = UUID()
let name: String
let price: Double
let description: String
let imageName: String
let isInStock: Bool
let isPopular: Bool
}
struct ProductListView: View {
// 产品数据
@State private var products: [Product] = [
Product(name: "iPhone 15 Pro", price: 7999, description: "最新款 iPhone,搭载 A17 Pro 芯片", imageName: "iphone", isInStock: true, isPopular: true),
Product(name: "iPad Air", price: 4799, description: "轻薄便携的平板电脑", imageName: "ipad", isInStock: true, isPopular: true),
Product(name: "MacBook Air", price: 8999, description: "轻薄便携的笔记本电脑", imageName: "macbook", isInStock: false, isPopular: true),
Product(name: "Apple Watch", price: 2999, description: "智能手表,健康助手", imageName: "watch", isInStock: true, isPopular: false),
Product(name: "AirPods Pro", price: 1899, description: "主动降噪耳机", imageName: "airpods", isInStock: true, isPopular: false)
]
// 状态
@State private var isRefreshing = false
@State private var isLoadingMore = false
var body: some View {
List {
// 热门产品分组
Section("热门产品") {
ForEach(products.filter { $0.isPopular }) { product in
ProductRow(product: product)
}
}
// 新品分组
Section("新品") {
ForEach(products.filter { !$0.isPopular }) { product in
ProductRow(product: product)
}
// 加载更多
if isLoadingMore {
HStack {
Spacer()
ProgressView()
Spacer()
}
.padding()
} else {
Button("加载更多") {
loadMoreProducts()
}
.frame(maxWidth: .infinity)
.padding()
}
}
}
.navigationTitle("产品列表")
.refreshable {
refreshProducts()
}
}
// 产品行视图
struct ProductRow: View {
let product: Product
var body: some View {
HStack(spacing: 16) {
// 产品图片
Image(systemName: product.imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 80)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
// 产品信息
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(product.name)
.font(.headline)
Spacer()
Text("¥\(product.price)")
.font(.headline)
.foregroundStyle(.blue)
}
Text(product.description)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
// 库存状态
HStack {
Text(product.isInStock ? "有货" : "缺货")
.font(.caption)
.padding(4)
.background(product.isInStock ? Color.green.opacity(0.1) : Color.red.opacity(0.1))
.foregroundStyle(product.isInStock ? .green : .red)
.cornerRadius(4)
}
}
}
.padding(.vertical, 8)
}
}
// 刷新产品
private func refreshProducts() {
isRefreshing = true
// 模拟网络请求
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// 刷新数据
isRefreshing = false
}
}
// 加载更多产品
private func loadMoreProducts() {
isLoadingMore = true
// 模拟网络请求
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// 添加更多产品
let newProducts = [
Product(name: "AirPods Max", price: 4399, description: "高端头戴式耳机", imageName: "headphones", isInStock: true, isPopular: false),
Product(name: "HomePod", price: 2299, description: "智能音箱", imageName: "speaker", isInStock: false, isPopular: false)
]
products.append(contentsOf: newProducts)
isLoadingMore = false
}
}
}
#Preview {
NavigationStack {
ProductListView()
}
}
代码解析
- Product 模型:包含产品的各种属性
- @State:用于管理产品数据和加载状态
- List 和 Section:用于分组显示产品
- ForEach:用于动态生成产品行
- ProductRow:自定义产品行视图
- refreshable:添加下拉刷新功能
- 加载更多:实现分页加载
- HStack 和 VStack:用于布局产品信息
技术点总结
List 组件
- 核心功能:显示有序数据集合,自动处理滚动和复用
- 数据要求:可以使用数组、Identifiable 对象或显式 ID
- 样式选项:plain、grouped、insetGrouped、sidebar、automatic
- 编辑功能:支持删除、移动操作
- 性能特点:适合中等大小的列表,自动优化渲染
Section 分组
- 作用:将列表内容逻辑分组
- 组成:可以包含标题和页脚
- 适用场景:需要逻辑分类的列表,如设置页面、联系人列表
ForEach 动态列表
- 核心作用:根据数据动态生成视图
- 使用方式:支持范围、Identifiable 对象、显式 ID
- 性能考量:对于大量数据,建议使用 LazyVStack
- 最佳实践:使用稳定的 ID,避免在闭包中进行复杂计算
ScrollView 滚动视图
- 灵活性:可以包含任何类型的视图
- 方向:支持垂直、水平、双向滚动
- 刷新:通过 refreshable 添加下拉刷新
- 滚动条:可以控制是否显示滚动指示器
懒加载容器
- LazyVStack:垂直方向的懒加载容器
- LazyHStack:水平方向的懒加载容器
- 核心优势:只在需要时创建视图,显著提升性能
- 适用场景:包含大量项目的列表或网格
性能优化建议
- 使用合适的容器:少量内容用 VStack/HStack,大量内容用 LazyVStack/LazyHStack
- 稳定的 ID:为 ForEach 提供稳定的标识符
- 避免复杂计算:将计算移到视图外部
- 合理使用 List:对于标准列表界面,List 提供了更好的用户体验
- 分页加载:对于非常长的列表,实现分页加载机制
参考资料
- SwiftUI 官方文档
- Apple Developer Documentation: List
- Apple Developer Documentation: ScrollView
- Apple Developer Documentation: LazyVStack
- WWDC 2020: Lists in SwiftUI
本内容为《SwiftUI 进阶》第六章,欢迎关注后续更新。