首发: cc log
译自: Enumerating elements in ForEach
假设我们使用SwiftUIlist展示数组中的内容,我们使用ForEach。
struct PeopleList:View {
var people: [Person]
var body: some View {
List {
ForEach(people) { person in
Text(person.name)
}
}
}
}
Person
是遵循Identifiable
的结构体:
struct Person: Identifiable {
var id: UUID = UUID()
var name: String
}
FrEach
使用Identifiable
来确保数组改变的时,知晓要插入的元素,或者删除的元素,以此来实施正确的动画。
ForEach中元素的序号
如果我们需要像下图一样,需要展示数组的序号。
有两个方案:
- 使用
enumerated()
,为每个元素提供一个(offset: Int, element: element)
的元组。 - 另外,使用
zip(1…, people)
,提供一样的元组, 但是我们定制起始数子,而不是固定为0。
我更喜欢zip:
ForEach(zip(1…, people)) { number, person in
Text("(number). (person.name)")
}
但是这样不会编译通过
- 传给
ForEach
必须是一个randomaccesscollection
,但是zip
产出的是一个Sequence
。 我们通过zip过的序列转换回数组来fix。 - 序列的元素类型,(Int, Person)不遵循
Identifiable
的协议——这个没法修复,元组不能遵循协议。
因此我们需要使用href="https://zhuanlan.zhihu.co
m/p/105491886/[https://developer.apple.com/documentation/swiftui/foreach/3364100-init">另外的ForEach初始化方法init(_:id:content:) 来使用key path显示的指定元素的ID。这里例子中是.1.id
,其中.1
表示元组中第二个元素person,.id
自然是person的唯一值key。
如此才能正常工作:
ForEach(Array(zip(1…, people)), id: .1.id) { number, person in
Text(“(number). (person.name)")
}
让代码更易读易用(Clarity at the point of use)
上述的代码明显不太易读,特别是.1
和Array(…)
的部分,为了增加易读性(Clarity at the point of use), 我们可以再进一步。
可以给Sequence
增加一个扩展:
extension Sequence {
/// Numbers the elements in `self`, starting with the specified number.
/// - Returns: An array of (Int, Element) pairs.
func numbered(startingAt start: Int = 1) -> [(number: Int, element: Element)] {
Array(zip(start..., self))
}
}
这样用,会让代码清晰一点
ForEach(people.numbered(), id: .element.id) { number, person in
Text(“(number). (person.name)”)
}
不使用key path
元组不能遵循协议Identifiable
,因此key path不能丢弃,但是我们可以能够动态查找成员的结构体来抽离。
@dynamicMemberLookup
struct Numbered<Element> {
var number: Int
var element: Element
subscript<T>(dynamicMember keyPath: WritableKeyPath<Element, T>) -> T {
get { element[keyPath: keyPath] }
set { element[keyPath: keyPath] = newValue }
}
}
根据Min Kim的建议,这里我们使用基于key path动态查找下标器, 然后改变numbered(startingAt:)
方法:
extension Sequence {
func numbered(startingAt start: Int = 1) -> [Numbered<Element>] {
zip(start..., self)
.map { Numbered(number: $0.0, element: $0.1) }
}
}
然后,当element能够Identifiable
时候,Numbered
自然也能够遵循Identifiable
协议。
extension Numbered: Identifiable where Element: Identifiable {
var id: Element.ID { element.id }
}
这样我们就能在使用ForEach
的时候省略key path了。
ForEach(people.numbered()) { numberedPerson in
Text(“(numberedPerson.number). (numberedPerson.name)”)
}
上述numberedPerson
类型是Numbered<Person>
,但是行为却很像Person
, 只是多了number
的属性, 如果没有动态属性查找dynamicMemberLookup
,我们只能这样numberedPerson.element.name
获取了,这样只能获取属性,对方法不行。
在这里,不得不惊叹Swift的精巧与强大,或者说,把事情搞复杂了?