©️ OverlookArt

List

List 是一种复杂的容器类型,可以通过为列表中的行提供单独的视图来构建列表,或者使用ForEach 来遍历一组数据来构建列表。您还可以混合这些策略,混合任意数量的单个视图和 ForEach 结构。

展示数据

  1. 创建一个结构体,并遵守 Identifiable 协议。 该结构体用于表示列表数据模型。

    1struct DataModel: Identifiable {
    2    var id: UUID = UUID()
    3    var title: String = ""
    4    var subtitle: String = ""
    5}
    
    Important

    列表中的成员必须拥有唯一标识。唯一标识符允许SwiftUI为基础数据的变化自动生成动画,如插入、删除和移动。识别列表成员,要么遵守 Identifiable 协议,要么像 DataModel 一样,要么通过提供带有该类型唯一属性的关键路径的id参数。填充上述列表的ForEach取决于这种行为,列表初始化器也依赖于使用 RandomAccessCollection 的成员进行迭代。

  2. 在 View 中声明一个数组变量,用户存放 DataModel 类型的元素。作为 ListView 展示内容的数据源。

    1struct MyView: View {
    2    var datas: [DataModel] = [
    3        DataModel(title: "My Title", subtitle:"subtitle"),
    4        DataModel(title: "标题", subtitle: "子标题")
    5    ]
    6    var body: some View { ... }
    7}
    
  3. 在视图的 body 中声明 ListView,并在其内部通过 ForEach 遍历列表的数据源 datas,并为每条数据声明 Text 来展示数据的 title

     1struct MyView: View {
     2    var datas: [DataModel] = [ ... ]
     3    var body: some View {
     4        List {
     5            ForEach(datas) { item in
     6                Text(item.title)
     7            }
     8        }
     9    }
    10}
    

自定义行视图

List 内展示复杂内容,使用自定义 View 将更多的视图组合成更复杂的东西。随着行视图变得更加复杂,将视图重构为单独的视图结构,传递行需要渲染的数据。

  1. 创建一个自定视图 ListRowView,声明一个要渲染的数据变量,在内部的视图用数据进行渲染。

     1struct ListRowView: View {
     2    var model: DataModel
     3    var body: some View {
     4        VStack(alignment: .leading, spacing: 8){
     5            Text(model.title)
     6                .font(.headline)
     7            Label(model.subtitle, systemImage: "exclamationmark.circle")
     8                .font(.subheadline)
     9        }
    10    }
    11}
    
  2. List 内的 ForEach 中使用自定义行视图 ListRowView 进行列表视图的渲染

     1struct MyView: View {
     2    var datas: [DataModel] = [ ... ]
     3    var body: some View {
     4        List {
     5            ForEach(datas) { item in
     6                ListRowView(model: item)
     7            }
     8        }
     9    }
    10}
    

列表分组

列表视图还可以显示具有层次结构的数据,将相关数据分组到部分中。

  1. 创建一个 SectionDataModel 结构体,表示分组数据。items 属性表示分组所有行的数据。

    1struct SectionDataModel : Identifiable {
    2    var sectionId: UUID = UUID()
    3    var sectionName: String = ""
    4    var items: [DataModel] = []
    5}
    
  2. 将列表数据结构改造为二级数组结构,

     1struct MyView: View {
     2        var datas: [SectionDataModel] = [
     3            SectionDataModel(sectionName: "分组1", items: [
     4                DataModel(title: "My Title", subtitle: "subtitle"),
     5                DataModel(title: "标题", subtitle: "子标题")
     6            ]),
     7            SectionDataModel(sectionName: "小池--杨万里", items: [
     8                DataModel(title: "泉眼无声惜细流", subtitle: "树阴照水爱晴柔"),
     9                DataModel(title: "小荷才露尖尖角", subtitle: "早有蜻蜓立上头")
    10            ])
    11        ]
    12        var body: some View { ... }
    13    }
    
  3. 使用 Section 视图为列表中的数据提供层次结构。

     1struct MyView: View {
     2    var datas: [SectionDataModel] = [ ... ]
     3    var body: some View {
     4        List {
     5            ForEach(datas) { section in
     6                Section(header: Text(section.sectionName)) {
     7                    ForEach(section.items) { item in
     8                        ListRowView(model: item)
     9                    }
    10                }
    11            }
    12        }
    13    }
    14}
    

列表导航

List 必须被包含 在 NavigationView 中, 使用 NavigationLink 导航至下一个 View。

通过用 NavigationView 包装列表来设置基于导航的用户界面。NavigationLink 的实例包裹列表的行,以提供目标视图,以便在用户点击该行时导航。

1NavigationView {
2    MyView()
3}
 1struct MyView: View {
 2    var datas: [SectionDataModel] = [ ... ]
 3    var body: some View {
 4        List {
 5            ForEach(datas) { section in
 6                Section(header: Text(section.sectionName)) {
 7                    ForEach(section.items) { item in
 8                        NavigationLink(destination: DetailView()) {
 9                            ListRowView(model: item)
10                        }
11                    }
12                }
13            }
14        }
15    }
16}

列表样式

调用 View 协议的 listStyle(_:) 方法来改变列表视图的展示样式。 ListStyle 有下面几种静态变量:

  • automatic:描述平台默认行为和列表外观的列表样式。
  • plain:描述普通列表行为和外观的列表样式。页眉和页脚将悬停在列表视图的顶部和底部。
  • grouped:描述分组列表行为和外观的列表样式。在iOS上,分组列表样式显示比普通列表样式更大的页眉和页脚,这在视觉上使不同部分的成员保持距离。
  • insetGrouped:描述嵌集分组列表的行为和外观的列表样式。在iOS上,内嵌分组列表样式显示连续的背景颜色,该颜色从部分标题延伸到部分中列表项的两侧,一直延伸到部分页脚。这在视觉上将项目分组到比内嵌或分组样式更大的程度。
1List {
2    ForEach(datas) { item in
3        ListRowView(model: item)
4    }
5}.listStyle(.automatic)

行内样式

  • 使用 listRowInsets(_:) 更改列表中行内容的边距。
  • 使用 listItemTint(_:) 设置行内特定内容关联的固定色调。
  • 使用 listRowBackground(_:) 设置行的自定义背景视图。
  • 使用 badge(_:) 在行内右侧展示徽章。设置徽章会导致行内背景失效。
 1...
 2List {
 3    ForEach(datas) { item in
 4        ListRowView(model: item)
 5            .listRowInsets(.init(top: 8, leading: 8, bottom: 8, trailing: 8))
 6            .listItemTint(.green)
 7            .listRowBackground(Color.purple.opacity(0.1))
 8            .badge("❤️")
 9    }
10}
11...

分割线样式

  • 使用 listRowSeparatorTint(_:edges:) 设置分割线颜色
  • 使用 listRowSeparator(_:edges:) 设置分割线显示隐藏
  • 使用 listSectionSeparatorTint(_:edges:) 设置分组分割线颜色
  • 使用 listSectionSeparator(_:edges:) 设置分组分割线显示隐藏
1List {
2    Section(header: Text("分组标题")){
3        ListRowView()
4            .listRowSeparatorTint(.yellow, edges: .all)
5            .listRowSeparator(.hidden, edges: .all)
6    }
7    .listSectionSeparatorTint(.cyan, edges: .all)
8    .listSectionSeparator(.hidden, edges: .top)
9}

侧滑操作

使用 swipeActions(edge:allowsFullSwipe:content:) 将自定义滑动操作添加到列表中的行中。

方法定义

1func swipeActions<T>(
2    edge: HorizontalEdge = .trailing,
3    allowsFullSwipe: Bool = true,
4    @ViewBuilder content: () -> T
5) -> some View where T : View

参数说明

  • edge: 侧滑操作的起始位置。默认值是 HorizontalEdge.trailing。.leading从左侧开始滑动,.trailing从右侧开始滑动。
  • allowsFullSwipe: 完全侧滑是否自动执行第一个操作。默认值为 true。
  • content: 滑动操作的内容。一般为 Button 控件。

使用示例

 1List {
 2    ListRowView()
 3        .swipeActions(edge: .leading, allowsFullSwipe: true) {
 4            Button {
 5                debugPrint("Read Action")
 6            } label: {
 7                Label("Read", systemImage: "envelope.open")
 8            }
 9            .tint(.blue)
10            Button { 
11                debugPrint("Unread Action")
12            } label: {
13                Label("Unread", systemImage: "envelope.badge")
14            }
15        }
16}