---
title: "【译】SwiftUI中使用ForEach枚举列表"
description: "在Swift中，使用dynamicMemberLookup配合key path简化SwiftUI 中ForEach中数字下标的获取"
url: "https://hicc.pro/p/swift/foreach-enumerated"
author: "tyler wang"
published: "2020-02-06T07:43:42.000Z"
updated: "2021-07-04T18:34:08.000Z"
category: "Swift"
tags:
  - "Swift"
  - "SwiftUI"
  - "dynamicMemerLookup"
image: "https://image.hicc.pro/pro/20210703152252Fhm7VbEvl674atbezeWQEf4bW8Czo2uVXyBH4E8H2hpj7bcXr.jpg"
lang: "zh-CN"
site: "Hipo日志"
---

# 【译】SwiftUI中使用ForEach枚举列表

译自:  [Enumerating elements in ForEach](https://oleb.net/2020/foreach-enumerated/?utm_campaign=iOS%2BDev%2BWeekly&utm_medium=email&utm_source=iOS%2BDev%2BWeekly%2BIssue%2B441) 

---

假设我们使用[SwiftUI](https://developer.apple.com/documentation/swiftui)list展示数组中的内容，我们使用[ForEach](https://developer.apple.com/documentation/swiftui/foreach)。

```swift
struct PeopleList:View {
    var people: [Person]
    
    var body: some View {
        List {
            ForEach(people) { person in
                Text(person.name)
            }
        }
    }
}
```

![](https://image.hicc.pro/hicc_me73668650_foreach-list-unnumbered-1325px.png)

`Person`是遵循[`Identifiable`](https://developer.apple.com/documentation/swift/identifiable)的结构体：

```swift
struct Person: Identifiable {
    var id: UUID = UUID()
    var name: String
}
```

`FrEach`使用`Identifiable`来确保数组改变的时，知晓要插入的元素，或者删除的元素，以此来实施正确的动画。

### ForEach中元素的序号

如果我们需要像下图一样，需要展示数组的序号。

![](https://image.hicc.pro/hicc_me1127578851_foreach-list-numbered-1325px.png)

有两个方案：

- 使用[`enumerated()`](https://developer.apple.com/documentation/swift/sequence/1641222-enumerated)，为每个元素提供一个`(offset: Int, element: element)`的元组。
- 另外，使用[`zip(1…, people)`](https://developer.apple.com/documentation/swift/1541125-zip)，提供一样的元组， 但是我们定制起始数子，而不是固定为0。

我更喜欢zip：

```swift
ForEach(zip(1…, people)) { number, person in
  Text("\(number). \(person.name)")
}
```

但是这样不会编译通过

![](https://image.hicc.pro/hicc_me440446575_79C8276C-4E04-42A9-A454-D4EC2DCC4B36_--.png)

1. 传给`ForEach`必须是一个[`randomaccesscollection`](https://developer.apple.com/documentation/swift/randomaccesscollection)，但是`zip`产出的是一个[`Sequence`](https://developer.apple.com/documentation/swift/sequence)。 我们通过zip过的序列转换回数组来fix。
2. 序列的元素类型，(Int, Person)不遵循`Identifiable`的协议——这个没法修复，元组不能遵循协议。

因此我们需要使用[另外的`ForEach`初始化方法`init(_:id:content:)` ]([https://developer.apple.com/documentation/swiftui/foreach/3364100-init)来使用[key path](https://docs.swift.org/swift-book/ReferenceManual/Expressions.html#ID563)显示的指定元素的ID。这里例子中是`\.1.id`，其中`.1`表示元组中第二个元素person，`.id`自然是person的唯一值key。

如此才能正常工作：

```swift
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](https://swift.org/documentation/api-design-guidelines/#fundamentals)）, 我们可以再进一步。

可以给`Sequence`增加一个扩展：

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

这样用，会让代码清晰一点

```swift
ForEach(people.numbered(), id: \.element.id) { number, person in
  Text(“\(number). \(person.name)”)
}
```

### 不使用key path
元组不能遵循协议`Identifiable`，因此key path不能丢弃，但是我们可以能够动态查找成员的结构体来抽离。

```swift
@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的建议](https://twitter.com/minmkim/status/1218217741406044162)，这里我们使用[基于key path动态查找下标器](https://github.com/apple/swift-evolution/blob/master/proposals/0252-keypath-dynamic-member-lookup.md)， 然后改变`numbered(startingAt:)`方法：

```swift
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`协议。

```swift
extension Numbered: Identifiable where Element: Identifiable {
  var id: Element.ID { element.id }
}
```

这样我们就能在使用`ForEach`的时候省略key path了。

```swift
ForEach(people.numbered()) { numberedPerson in
  Text(“\(numberedPerson.number). \(numberedPerson.name)”)
}
```

上述`numberedPerson`类型是`Numbered<Person>`，但是行为却很像`Person`， 只是多了`number`的属性， 如果没有动态属性查找`dynamicMemberLookup`，我们只能这样`numberedPerson.element.name`获取了，这样只能获取属性，对方法不行。

- - - -

在这里，不得不惊叹Swift的精巧与强大，或者说，把事情搞复杂了？

---

原文 / Source: <https://hicc.pro/p/swift/foreach-enumerated>
