《SwiftUI 进阶第6章:列表与滚动视图》

8 阅读7分钟

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 的性能优化技巧:

  1. 使用稳定的 ID:避免使用 UUID() 作为临时 ID
  2. 使用 LazyVStack:对于非常长的列表,考虑使用 ScrollView + LazyVStack
  3. 避免在 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

懒加载容器介绍

LazyVStackLazyHStack 是懒加载的栈容器,只在需要时创建视图,非常适合处理大量数据。

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功能丰富,自带滚动样式固定标准列表界面

实战:创建一个产品展示列表

需求分析

创建一个产品展示列表,包含以下功能:

  1. 产品图片、名称、价格、描述
  2. 分组显示(热门产品、新品)
  3. 下拉刷新
  4. 加载更多
  5. 产品状态(有货/缺货)

代码实现

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()
    }
}

代码解析

  1. Product 模型:包含产品的各种属性
  2. @State:用于管理产品数据和加载状态
  3. List 和 Section:用于分组显示产品
  4. ForEach:用于动态生成产品行
  5. ProductRow:自定义产品行视图
  6. refreshable:添加下拉刷新功能
  7. 加载更多:实现分页加载
  8. HStack 和 VStack:用于布局产品信息

技术点总结

List 组件

  • 核心功能:显示有序数据集合,自动处理滚动和复用
  • 数据要求:可以使用数组、Identifiable 对象或显式 ID
  • 样式选项:plain、grouped、insetGrouped、sidebar、automatic
  • 编辑功能:支持删除、移动操作
  • 性能特点:适合中等大小的列表,自动优化渲染

Section 分组

  • 作用:将列表内容逻辑分组
  • 组成:可以包含标题和页脚
  • 适用场景:需要逻辑分类的列表,如设置页面、联系人列表

ForEach 动态列表

  • 核心作用:根据数据动态生成视图
  • 使用方式:支持范围、Identifiable 对象、显式 ID
  • 性能考量:对于大量数据,建议使用 LazyVStack
  • 最佳实践:使用稳定的 ID,避免在闭包中进行复杂计算

ScrollView 滚动视图

  • 灵活性:可以包含任何类型的视图
  • 方向:支持垂直、水平、双向滚动
  • 刷新:通过 refreshable 添加下拉刷新
  • 滚动条:可以控制是否显示滚动指示器

懒加载容器

  • LazyVStack:垂直方向的懒加载容器
  • LazyHStack:水平方向的懒加载容器
  • 核心优势:只在需要时创建视图,显著提升性能
  • 适用场景:包含大量项目的列表或网格

性能优化建议

  1. 使用合适的容器:少量内容用 VStack/HStack,大量内容用 LazyVStack/LazyHStack
  2. 稳定的 ID:为 ForEach 提供稳定的标识符
  3. 避免复杂计算:将计算移到视图外部
  4. 合理使用 List:对于标准列表界面,List 提供了更好的用户体验
  5. 分页加载:对于非常长的列表,实现分页加载机制

参考资料


本内容为《SwiftUI 进阶》第六章,欢迎关注后续更新。