• 赚钱入口【需求资源】限时招募流量主、渠道主,站长合作;【合作模式】CPS长期分成,一次推广永久有收益。主动打款,不扣量;

枚举ForEach中的元素

iOS rin, seun 1年前 (2020-06-20) 229次浏览 0个评论

假设我们要在SwiftUI列表中显示数组的内容。我们可以这样ForEach

struct PeopleList: View {
  var people: [Person]

  var body: some View {
    List {
      ForEach(people) { person in
        Text(person.name)
      }
    }
  }
}

枚举ForEach中的元素普通的,未编号的列表。

Person是符合Identifiable协议的结构:

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

ForEach使用Identifiable一致性确定输入数组更改时在哪些位置插入或删除了元素,以便正确设置这些更改的动画。

编号的 ForEach

现在假设我们要为列表中的项目编号,如以下屏幕截图所示:

枚举ForEach中的元素编号列表。

我们可以尝试以下方法之一:

  • 调用enumerated()我们传递给的数组,该数组将为每个元素ForEach生成形式的元组(offset: Int, element: Element)
  • 或者,zip(1..., people)生成相同形状的元组(尽管没有标签),但是允许我们选择一个不同于0的起始数字。

我通常喜欢zipenumerated这个原因,让我们在这里使用它:

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

这不能编译有两个原因:

  1. 传递给的集合ForEach必须为RandomAccessCollection,但zip产生Sequence。我们可以通过将压缩后的序列转换回数组来解决此问题。
  2. 带编号的序列的元素类型(Int, Person)不再符合Identifiable—并且不再符合,因为元组不符合协议。这意味着我们需要使用不同的ForEach初始化器,该初始化器使我们可以将键路径传递到元素的标识符字段。在此示例中\.1.id,正确的键路径是,其中.1选择元组中的第二个元素并.id指定Person类型的属性。

工作代码如下所示:

ForEach(Array(zip(1..., people)), id: \.1.id) { number, person in
  Text("\(number). \(person.name)")
}

可读性

乍一看还不清楚。我特别不喜欢.1关键路径中的,并且Array(…)包装器只是噪音。为了提高使用时的清晰度,我写了一个小助手作为扩展,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))
  }
}

这使call stack变得更好:

ForEach(people.numbered(), id: \.element.id) { number, person in
  Text("\(number). \(person.name)")
}

删除关键路径

关键路径更具可读性,但是不幸的是我们不能完全忽略它。我们无法创建元组Identifiable,但是我们可以引入一个自定义结构,将其用作编号集合的元素类型:

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

请注意,我添加了一个基于键路径的动态成员查找下标。严格来说,这不是必须的,但它将允许客户Numbered<Person>几乎像普通值一样使用值Person。非常感谢闵金(Min Kim)提出的建议,这对我而言从未发生过。

让我们更改numbered(startingAt:)方法以使用新类型:

extension Sequence {
  func numbered(startingAt start: Int = 1) -> [Numbered<Element>] {
    zip(start..., self)
      .map { Numbered(number: $0.0, element: $0.1) }
  }
}

现在,我们可以在其元素类型Numbered为时有条件地使结构符合:IdentifiableIdentifiable

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

这使我们可以省略键路径,并返回到ForEach最初使用的初始化程序:

ForEach(people.numbered()) { numberedPerson in
  Text("\(numberedPerson.number). \(numberedPerson.name)")
}

这是我们上面添加的基于密钥路径的成员查找显示其强度的地方。的numberedPerson变量的类型的Numbered<Person>,但它几乎如同普通Person具有添加的结构number属性,因为编译器转发不存在的字段访问包裹Person值在完全类型安全的方式。没有成员查找下标,我们将不得不编写numberedPerson.element.name。这仅适用于访问属性,不适用于方法。

喜欢 (0)

您必须 登录 才能发表评论!