SwiftUI - list

133 阅读1分钟

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,执行第一个

iShot_2022-12-13_16.43.47.gif

我们将上面的代码改造如下:

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")
  ])
]

通过如下方式构建视图

iShot_2022-12-13_16.59.13.gif

List(expandWeather, id: \.self, children: \.weathers) { wealther in
    Label(wealther.name, systemImage: wealther.icon)
}

8.ForEach 和 List 配合使用

可以轻松地对列表进行编辑操作:delete、move。还是以 weathers 作为数据源,我们构建列表视图:

iShot_2022-12-13_17.12.15.gif

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 数据,通过它对多条数据同时操作