List
List 为 SwiftUI 中的列表,类似于 UIKit 中的 UITableView,List 中的每一项类似于 UITableViewCell。由于 List 比较复杂,故本文我们通篇只讨论 List 这一种控件。
静态 List
用 List 做静态列表非常容易,使用类似 VStack 的写法即可。
struct ContentView: View { var body: some View { List { Text("Hello SwiftUI") Image(systemName: "clock") Text("Hello SwiftUI") } } }
分组
类似 UITableView,我们也可以对 List 进行分组显示。
struct ContentView: View { var body: some View { List { Section(header: Text("UIKit"), footer: Text("表格")) { Text("UITableView") } Section(header: Text("SwiftUI"), footer: Text("列表")) { Text("List") } } } }
每个分组由 header+body+footer 组成。
在先前写 UITableView 时我们常常会用到 Grouped 样式,这种样式的实现非常简单,在 List 后面加一个 .listStyle(GroupedListStyle()) 修饰符即可。
struct ContentView: View { var body: some View { List { Section(header: Text("UIKit"), footer: Text("表格")) { Text("UITableView") } Section(header: Text("SwiftUI"), footer: Text("列表")) { Text("List") } }.listStyle(GroupedListStyle()) } }
在 iOS 13.2 之后,新增了一个 InsetGrouped 样式,需要我们在分组后强制使用 .environment(\.horizontalSizeClass, .regular)。
struct ContentView: View { var body: some View { List { Section(header: Text("UIKit"), footer: Text("表格")) { Text("UITableView") } Section(header: Text("SwiftUI"), footer: Text("列表")) { Text("List") } }.listStyle(GroupedListStyle()) .environment(\.horizontalSizeClass, .regular) } }
如果需要修改分组的背景色,直接设置 .background 即可。
struct ContentView: View { var body: some View { List { Section(header: //内容宽度 HStack { Text("Header") .font(.headline) .foregroundColor(.white) .padding() Spacer() } .background(Color.orange) //修改分组背景色 //边距为0 .listRowInsets( EdgeInsets( top: 0, leading: 0, bottom: 0, trailing: 0)) ) { Text("1") Text("2") Text("3") Text("4") Text("5") Text("6") } } } }
背景
修改每一行的背景色也十分容易。
struct ContentView: View { var body: some View { List { ForEach(0..<10) { Text("Row \($0)") } //设置背景,可以是颜色或者形状 .listRowBackground(Color.red) } } }
填充
假如我们要增加行高该怎么做呢?我们可以 listRowInsets 来设置周边填充大小,就能间接增加行高了。
struct ContentView: View { var body: some View { List { ForEach(0..<10) { Text("Row \($0)") } //设置周边填充大小 .listRowInsets(EdgeInsets(top: 30, leading: 60, bottom: 30, trailing: 0)) } } }
.listRowInsets应当直接设置在行的内容上而不是设置在 List 上。
分割线
目前来说,每行之间的分割线的操作还是要借助 UITableView。
struct ContentView: View { var body: some View { List(0..<10) { Text("Row \($0)") }.onAppear { UITableView.appearance().separatorStyle = .none } //去除分割线 } }
struct ContentView: View { var body: some View { List(0..<10) { Text("Row \($0)") }.onAppear { UITableView.appearance().separatorColor = .red } //设置分割线颜色 } }
struct ContentView: View { var body: some View { List(0..<10) { Text("Row \($0)") }.onAppear { UITableView.appearance().tableFooterView = UIView() } //去除多余行 } }
动态 List
动态 List 的构建只需要直接遍历数组即可,List 的数据源必须实现 Identifiable 协议,表示数据需要的唯一标识,一般直接设置 id 即可。
struct User: Identifiable { var id = UUID() var name: String } struct ContentView: View { //数据源 let users = [User(name:"ZhangSan"), User(name:"LiSi"), User(name:"WangWu")] var body: some View { //构造列表 List(users) { user in Text(user.name) } } }
同样我们可以用 ForEach 方式来构建 List,需要传一个 KeyPath 参数表示唯一标识,一般传递 \.self 即可。
struct ContentView: View { let names = ["zhangsan", "list", "wangwu"] var body: some View { List{ ForEach(names, id: \.self) { name in Text(name) } } } }
在之前的 UIKit 中,我们常常会在 UITableView 中自定义 cell。下面我们通过一个复杂一些的案例来演示如何在 List 中自定义 cell。
struct ContentView: View { //数据源 let titles = ["2020.8.17", "2020.8.18","2020.8.19","2020.8.20","2020.8.21","2020.8.22","2020.8.23"] let subtitles = ["周一", "周二","周三","周四","周五","周六","周日"] let details = ["下雨", "晴天","有雾","多云","阴天","下雪","大风"] var body: some View { //遍历数据源构造Item List(0..<self.titles.count) { item in HStack { VStack { Text(self.titles[item]) .foregroundColor(.orange) .bold() .font(.title) Text(self.subtitles[item]) .foregroundColor(.gray) .font(.subheadline) }.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 50)) Text(self.details[item]) .foregroundColor(.blue) .font(.largeTitle) .italic() }.padding() } } }
编辑
与 UIKit 不同,SwiftUI 是严格的数据驱动框架,若我们要编辑 List 的内容只需要修改数据源。
增加
struct ContentView: View { //数据源 @State var names = ["ZhangSan", "LiSi", "WangWu"] var body: some View { VStack{ Button("Add"){ //增加 self.names += ["LouYu", "ZhaoLiu"] } List{ ForEach(names, id: \.self) { name in Text(name) } } } } }
删除
struct ContentView: View { //数据源 @State var names = ["ZhangSan", "LiSi", "WangWu"] var body: some View { List{ ForEach(names, id: \.self) { name in Text(name) } .onDelete(perform: delete(at:)) //删除,此时左滑会出现删除按钮 } } //删除时执行的函数 func delete(at offsets: IndexSet) { if let first = offsets.first { names.remove(at: first) } } }
删除也可以简写成如下形式。
struct ContentView: View { //数据源 @State var names = ["ZhangSan", "LiSi", "WangWu"] var body: some View { List{ ForEach(names, id: \.self) { name in Text(name) } .onDelete{ index in self.names.remove(at: index.first!) } } } }
移动
struct ContentView: View { //数据源 @State var names = ["ZhangSan", "LiSi", "WangWu"] var body: some View { NavigationView { List{ ForEach(names, id: \.self) { name in Text(name) } .onMove(perform: move(from:to:)) } //EditButton,点击后进入编辑模式 .navigationBarItems(trailing: EditButton()) .navigationBarTitle("移动List") } } //移动时执行的函数 func move(from: IndexSet, to: Int) { names.move(fromOffsets: from, toOffset: to) } }
移动同样可以简写如下。
struct ContentView: View { //数据源 @State var names = ["ZhangSan", "LiSi", "WangWu"] var body: some View { NavigationView { List{ ForEach(names, id: \.self) { name in Text(name) } .onMove{ (from, to) in self.names.move(fromOffsets: from, toOffset: to) } } .navigationBarItems(trailing: EditButton()) .navigationBarTitle("移动List") } } }