SwiftUI中ForEach的使用
4个月前 • 539次点击 • 来自 SwiftUI
ForEach使用Identifiable来确保数组改变的时,知晓要插入的元素,或者删除的元素,以此来实施正确的动画。
在常规模式下使用ForEach进行遍历时,数据Model需要继承Identifiable协议并提供id生成方法:
import SwiftUI
let peoples = [Person(name: "1", gender: "male"), Person(name: "2", gender: "female"), Person(name: "3", gender: "female"), Person(name: "4", gender: "male"), Person(name: "5", gender: "female")]
struct Person: Identifiable {
var id: UUID = UUID()
var name: String
var gender: String
}
struct PeopleList: View {
var body: some View {
List {
ForEach(peoples) { person in
HStack {
Text(person.name)
Text(person.gender)
}
}
}
}
}
1.提供id生成策略
Person是否可以不继承Identifiable?我们来看看ForEach:
/// A structure that computes views on demand from an underlying collection of
/// of identified data.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable {
/// The collection of underlying identified data.
public var data: Data
/// A function that can be used to generate content on demand given
/// underlying data.
public var content: (Data.Element) -> Content
}
可以看到,它的第二个参数ID : Hashable,Swift的所有基本类型(形如String,Int,Double,Bool)默认是可哈希化的,在此例中,我们可以这么改写:
import SwiftUI
let peoples = [Person(name: "1", gender: "male"), Person(name: "2", gender: "female"), Person(name: "3", gender: "female"), Person(name: "4", gender: "male"), Person(name: "5", gender: "female")]
struct Person {
var name: String
var gender: String
}
struct PeopleList: View {
var body: some View {
List {
ForEach(peoples, id: \.name) { person in
HStack {
Text(person.name)
Text(person.gender)
}
}
}
}
}
思考一下,是否可以使用:
ForEach(peoples, id: \.self)
答案是可以的,在使用**.self**的情况下,SwiftUI会调用Person所有的属性来生成hash值,那么这意味着Person需要实现 Hashable :
struct Person: Hashable {
var name: String
var gender: String
}
2.通过下标访问
集合中的数据类型不是Identifiable的话,ForEach可以通过Range下标访问的形式直接获取数据,如下:
struct PeopleList: View {
var body: some View {
List {
ForEach(0 ..< peoples.count) { i in
HStack {
Text(peoples[i].name)
Text(peoples[i].gender)
}
}
}
}
}
注意:
这种形式目前还不是特别稳定,在极少数的情况下,当你的数据集发生更改时,编译器有时候无法正确的通过下标访问到数据,据笔者查阅资料后得知,这是SwiftUI内部动态更新数据时因为缓存出现的偏差。
使用indices是更好的选择,同样是通过下标访问到数据,只需要如下改写:
struct PeopleList: View {
var body: some View {
List {
ForEach(peoples.indices) { i in
HStack {
Text(peoples[i].name)
Text(peoples[i].gender)
}
}
}
}
}
3.ForEach中元素的序号
在上例中,你已经可以获得下标值,但是,我们在此节更深入学习下其他的方式。
- enumerated()
- zip()
3.1 enumerated()
在以往的开发中,遍历集合一定是少不了enumerated(),同样,在ForEach中你也可以继续使用,只是,体验并不是很好:
struct PeopleList: View {
var body: some View {
List {
ForEach(Array(peoples.enumerated()), id: \.offset) { index, people in
HStack {
Text("\(index)")
Text(people.name)
Text(people.gender)
}
}
}
}
}
之所以说它体验不好是因为它的写法效率低下,直接通过peoples.enumerated()获取到的类型是EnumeratedSequence,它并不提供随机访问的功能:
@inlinable public func enumerated() -> EnumeratedSequence<Array<Element>>
而ForEach接受的是RandomAccessCollection,那么你就需要使用Array函数进行一层转换。
你也可以使用reversed()来得到RandomAccessCollection:
ForEach(peoples.enumerated().reversed(), id: \.offset)
但显然,数据集的元素顺序就是不可预测的。
3.2 zip
zip(1..., peoples),接受一个元组,第一个参数可用于定制起始数子,1...表示下标从1开始计算。
同样的,使用zip也存在与enumerated类似的效率问题:
struct Person {
var name: String
var gender: String
}
struct PeopleList: View {
var body: some View {
List {
ForEach(Array(zip(1..., peoples)), id: \.1.name) { index, people in
HStack {
Text("\(index)")
Text(people.name)
Text(people.gender)
}
}
}
}
}
例子中是 \.1.name ,其中 .1 表示元组中第二个元素person的属性name。