SwiftUI中ForEach的使用

4周前 57次点击 来自 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)
                }
            }
        }
    }
}

截屏2020-08-29 上午10.03.58

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)
                }
            }
        }
    }
}

截屏2020-08-29 上午10.55.17

之所以说它体验不好是因为它的写法效率低下,直接通过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。

参考链接:
SwiftUI, list with indices without enumerated

Card image cap
开发者雷

尘世间一个小小的开发者

技术文档 >> 系列应用 >>
热推应用

EntryS

学习Swift的入门教程
标签