1.基础构建方法
List 最基本的构建方法:
List {
Text("Sun")
Text("Cloud")
Text("Snow")
}
2.分组
Section
可以在 List 中构建分组列表,iOS 15.0 新增了一个改变分组 header 样式的修饰器:.headerProminence
,我们可以使用枚举值 .increased
使 header 变得更为醒目。
struct GroupedList: View {
@State private var isIncreased = false
var body: some View {
List {
Group {
Section("Weather") {
Text("Sun")
Text("Cloud")
Text("Snow")
}
Section("Animal") {
Text("Dog")
Text("Cat")
}
}
.headerProminence(
isIncreased ? .increased : .standard) // iOS 15.0+
}
.toolbar {
ToolbarItem(placement: .bottomBar) {
Toggle("isIncreased", isOn: $isIncreased)
.toggleStyle(.switch)
}
}
}
}
3.数据源
通常我们的列表并非静态,而是与动态的数据源绑定的。List 可以通过传入 data
来实现,不过这里的 data 需要遵循 Hashable 协议,这样才能保证数据的唯一性。
我们先对天气作如下定义:
struct Weather: Hashable {
let id = UUID()
let name: String
let icon: String
}
id
是协议要求实现的属性(遵循 Hashable 协议),我们也可以在实例初始化时传入 0, 1, 2, 3...只要是没有冲突的哈希值都可以。这里我们有个简单的处理方法,就是通过 UUID()
生成惟一值,在初始化时可以省去传参。
然后我们添加数据源:
@State private var weathers = [
Weather(name: "Sunshine", icon: "sun.max.fill"),
Weather(name: "Cloud", icon: "cloud"),
Weather(name: "Snow", icon: "snow"),
Weather(name: "Rain", icon: "cloud.rain.fill")
]
生成 List:
List(weathers, id: .self) { v in
Label(v.name, systemImage: v.icon)
}
4.搜索框
OS 15 新增了 .searchable
修饰器,可以在 NavigationView 中使用,我们这里的数据源 List 本身就处于 NavigationView 中,我们来看看如何为它添加搜索功能。
使用的方法很简单,我们只需将上面的的代码稍微改造:
@State private var searchText = ""
List(searchResults, id: .self) { v in
Label(v.name, systemImage: v.icon)
}
.searchable(text: $searchText, prompt: "Input something...")
var searchResults: [Weather] {
searchText.isEmpty ? weathers : weathers.filter { $0.name.contains(searchText) }
}
.searchable
有一个参数 placement
用于控制搜索框的显示位置,其默认值为 .automatic
,在不同平台有不同的表现。在 iPhone 上默认是隐藏在导航栏下方的,列表向下滑动时可见,向上滑动时会自动隐藏。如果我们需要让其一直可见,可以作如下修改:
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always) , prompt: "Input something...")
.searchable
还支持推荐功能,代码如下,具体效果请查看示例:
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always) , prompt: "Input something...") {
ForEach(searchResults, id: .self) { v in
Text("Looking for (v.name)?").searchCompletion(v.name)
Text("Looking for (Image(systemName: v.icon))").searchCompletion(v.name)
}
}
.onSubmit(of: .search) { // 点击推荐的建议或者键盘上的 return 键时会触发该事件
print("搜索🔍")
}
5.滑动编辑
搜索框的使用就介绍至此,下面我们来看看 iOS 15 针对 List 新增的滑动操作:
func swipeActions<T>(edge: HorizontalEdge = .trailing, allowsFullSwipe: Bool = true, content: () -> T) -> some View where T : View
edge
:滑动操作的位置,默认在列表行的右边allowsFullSwipe
:是否允许通过滑动手势来执行操作,如有多个 action,执行第一个
我们将上面的代码改造如下:
List(Array(searchResults.enumerated()), id: .offset) { i, v in
Label(v.name, systemImage: v.icon)
.swipeActions(edge: .leading, allowsFullSwipe: false) {
Button {
let random = Int(arc4random_uniform(UInt32(weathers.count)))
weathers.append(weathers[random])
} label: {
Image(systemName: "plus.circle")
}
.tint(.green)
}
.swipeActions(edge: .trailing) {
Button {
weathers.remove(at: i)
} label: {
Image(systemName: "delete.backward")
}
.tint(.red)
Button {
let random = Int(arc4random_uniform(UInt32(weathers.count)))
weathers[i] = weathers[random]
} label: {
Image(systemName: "arrow.clockwise")
}
.tint(.purple)
}
}
由于指定 allowsFullSwipe
为 false,所以右滑只能通过点击来新增行,而左滑则可以直接删除行。
6.单选或多选
ist 初始化方法中可以绑定选中值,支持单选或多选。
struct SelectionList: View {
struct Weather: Identifiable, Hashable {
let id = UUID()
let name: String
let icon: String
}
@State private var weathers = [
Weather(name: "Sunshine", icon: "sun.max.fill"),
Weather(name: "Cloud", icon: "cloud"),
Weather(name: "Snow", icon: "snow"),
Weather(name: "Rain", icon: "cloud.rain.fill")
]
@State private var singleSelection: UUID?
@State private var multiSelection = Set<UUID>()
var body: some View {
VStack {
GroupBox("单选") {
List(weathers, selection: $singleSelection) { // 单选
Label($0.name, systemImage: $0.icon)
}
}
GroupBox("多选") {
List(weathers, selection: $multiSelection) { // 多选
Label($0.name, systemImage: $0.icon)
}
}
}
.toolbar {
EditButton()
}
}
}
7.可展开列表
List 可以通过树形结构的数据源直接构建可展开的列表,比如我们定义如下可展开的天气对象:
struct ExpandWeather: Hashable {
let id = UUID()
var name: String
var icon: String
var weathers: [ExpandWeather]?
}
该结构体内嵌的 weathers 的元素类型就是它本身,而且是可选类型。我们构造如下数据:
let expandWeather: [ExpandWeather] = [
ExpandWeather(name: "Weather", icon: "", weathers: [
ExpandWeather(name: "Sunshine", icon: "sun.max.fill"),
ExpandWeather(name: "Cloud", icon: "cloud"),
ExpandWeather(name: "Snow", icon: "snow"),
ExpandWeather(name: "Rain", icon: "cloud.rain.fill")
])
]
通过如下方式构建视图
List(expandWeather, id: \.self, children: \.weathers) { wealther in
Label(wealther.name, systemImage: wealther.icon)
}
8.ForEach 和 List 配合使用
可以轻松地对列表进行编辑操作:delete、move。还是以 weathers 作为数据源,我们构建列表视图:
var body: some View {
List {
ForEach(weathers, id: .self) { v in
Label(v.name, systemImage: v.icon)
}
.onDelete(perform: onDelete)
.onMove(perform: onMove)
}
.navigationBarItems(trailing: EditButton())
}
func onDelete(offsets: IndexSet) {
weathers.remove(atOffsets: offsets)
}
func onMove(fromOffsets: IndexSet, toOffset: Int) {
weathers.move(fromOffsets: fromOffsets, toOffset: toOffset)
}
EditButton 终于登场了,试试对列表进行编辑操作。
这里的编辑操作只是单选操作,如果我们要多选呢?可以在 List 中绑定 selection 数据,通过它对多条数据同时操作