Saturday, November 16, 2024
HomeiOS DevelopmentEnumerating parts in ForEach – Ole Begemann

Enumerating parts in ForEach – Ole Begemann

[ad_1]

Suppose we need to show the contents of an array in a SwiftUI checklist. We will do that with ForEach:

struct PeopleList: View {
  var folks: [Person]

  var physique: some View {
    Checklist {
      ForEach(folks) { particular person in
        Textual content(particular person.title)
      }
    }
  }
}

iPhone showing a plain, unnumbered list of people
The plain, unnumbered checklist.

Individual is a struct that conforms to the Identifiable protocol:

struct Individual: Identifiable {
  var id: UUID = UUID()
  var title: String
}

ForEach makes use of the Identifiable conformance to find out the place parts have been inserted or deleted when the enter array modifications, so as animate these modifications appropriately.

Now suppose we need to quantity the gadgets within the checklist, as on this screenshot:


iPhone showing a numbered list of people
The numbered checklist.

We’d strive one in every of these approaches:

  • Name enumerated() on the array we move to ForEach, which produces a tuple of the shape (offset: Int, aspect: Component) for every aspect.

  • Alternatively, zip(1..., folks) produces tuples of the identical form (albeit with out the labels), however permits us to decide on a distinct beginning quantity than 0.

I often want zip over enumerated because of this, so let’s use it right here:

ForEach(zip(1..., folks)) { quantity, particular person in
  Textual content("(quantity). (particular person.title)")
}

This doesn’t compile for 2 causes:

  1. The gathering handed to ForEach have to be a RandomAccessCollection, however zip produces a Sequence. We will repair this by changing the zipped sequence again into an array.

  2. The aspect kind of the numbered sequence, (Int, Individual), not conforms to Identifiable — and might’t, as a result of tuples can’t conform to protocols.

    This implies we have to use a distinct ForEach initializer, which lets us move in a key path to the aspect’s identifier area. The right key path on this instance is .1.id, the place .1 selects the second aspect within the tuple and .id designates the property of the Individual kind.

The working code then appears to be like like this:

ForEach(Array(zip(1..., folks)), id: .1.id) { quantity, particular person in
  Textual content("(quantity). (particular person.title)")
}

It’s not tremendous clear what’s occurring there at a fast look; I significantly dislike the .1 in the important thing path, and the Array(…) wrapper is simply noise. To enhance readability on the level of use, I wrote a bit of helper as an extension on Sequence that provides labels to the tuple and hides among the internals:

extension Sequence {
  /// Numbers the weather in `self`, beginning with the required quantity.
  /// - Returns: An array of (Int, Component) pairs.
  func numbered(startingAt begin: Int = 1) -> [(number: Int, element: Element)] {
    Array(zip(begin..., self))
  }
}

This makes name websites fairly a bit nicer:

ForEach(folks.numbered(), id: .aspect.id) { quantity, particular person in
  Textual content("(quantity). (particular person.title)")
}

The important thing path is extra readable, nevertheless it’s unlucky that we are able to’t depart it out fully. We will’t make the tuple Identifiable, however we might introduce a customized struct that acts because the aspect kind for our numbered assortment:

@dynamicMemberLookup
struct Numbered<Component> {
  var quantity: Int
  var aspect: Component

  subscript<T>(dynamicMember keyPath: WritableKeyPath<Component, T>) -> T {
    get { aspect[keyPath: keyPath] }
    set { aspect[keyPath: keyPath] = newValue }
  }
}

Discover that I added a key-path based mostly dynamic member lookup subscript. This isn’t strictly needed, however it’ll enable purchasers to make use of a Numbered<Individual> worth virtually as if it have been a plain Individual. Many due to Min Kim for suggesting this, it hadn’t occured to me.

Let’s change the numbered(startingAt:) technique to make use of the brand new kind:

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

And now we are able to conditionally conform the Numbered struct to Identifiable when its aspect kind is Identifiable:

extension Numbered: Identifiable the place Component: Identifiable {
  var id: Component.ID { aspect.id }
}

This permits us to omit the important thing path and return to the ForEach initializer we used initially:

ForEach(folks.numbered()) { numberedPerson in
  Textual content("(numberedPerson.quantity). (numberedPerson.title)")
}

That is the place the key-path-based member lookup we added above exhibits its energy. The numberedPerson variable is of kind Numbered<Individual>, nevertheless it virtually behaves like a traditional Individual struct with an added quantity property, as a result of the compiler forwards non-existent area accesses to the wrapped Individual worth in a totally type-safe method. With out the member lookup subscript, we’d have to write down numberedPerson.aspect.title. This solely works for accessing properties, not strategies.



[ad_2]

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments