I'm using a ForEach
to display the contents of an array, then manually showing a divider between each element by checking the element index. Here's my code:
struct ContentView: View {
let animals = ["Apple", "Bear", "Cat", "Dog", "Elephant"]
var body: some View {
VStack {
/// array of tuples containing each element's index and the element itself
let enumerated = Array(zip(animals.indices, animals))
ForEach(enumerated, id: \.1) { index, animal in
Text(animal)
/// add a divider if the element isn't the last
if index != enumerated.count - 1 {
Divider()
.background(.blue)
}
}
}
}
}
Result:
This works, but I'd like a way to automatically add dividers everywhere without writing the Array(zip(animals.indices, animals))
every time. Here's what I have so far:
struct ForEachDividerView<Data, Content>: View where Data: RandomAccessCollection, Data.Element: Hashable, Content: View {
var data: Data
var content: (Data.Element) -> Content
var body: some View {
let enumerated = Array(zip(data.indices, data))
ForEach(enumerated, id: \.1) { index, data in
/// generate the view
content(data)
/// add a divider if the element isn't the last
if let index = index as? Int, index != enumerated.count - 1 {
Divider()
.background(.blue)
}
}
}
}
/// usage
ForEachDividerView(data: animals) { animal in
Text(animal)
}
This works great, isolating all the boilerplate zip
code and still getting the same result. However, this is only because animals
is an array of String
s, which conform to Hashable
— if the elements in my array didn't conform to Hashable
, it wouldn't work:
struct Person {
var name: String
}
struct ContentView: View {
let people: [Person] = [
.init(name: "Anna"),
.init(name: "Bob"),
.init(name: "Chris")
]
var body: some View {
VStack {
/// Error! Generic struct 'ForEachDividerView' requires that 'Person' conform to 'Hashable'
ForEachDividerView(data: people) { person in
Text(person.name)
}
}
}
}
That's why SwiftUI's ForEach
comes with an additional initializer, init(_:id:content:)
, that takes in a custom key path for extracting the ID. I'd like to take advantage of this initializer in my ForEachDividerView
, but I can't figure it out. Here's what I tried:
struct ForEachDividerView<Data, Content, ID>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
var data: Data
var id: KeyPath<Data.Element, ID>
var content: (Data.Element) -> Content
var body: some View {
let enumerated = Array(zip(data.indices, data))
/// Error! Invalid component of Swift key path
ForEach(enumerated, id: \.1.appending(path: id)) { index, data in
content(data)
if let index = index as? Int, index != enumerated.count - 1 {
Divider()
.background(.blue)
}
}
}
}
/// at least this part works...
ForEachDividerView(data: people, id: \.name) { person in
Text(person.name)
}
I tried using appending(path:)
to combine the first key path (which extracts the element from enumerated
) with the second key path (which gets the Hashable
property from the element), but I got Invalid component of Swift key path
.
How can I automatically add a divider in between the elements of a ForEach
, even when the element doesn't conform to Hashable
?
CodePudding user response:
Simple way
struct ContentView: View {
let animals = ["Apple", "Bear", "Cat", "Dog", "Elephant"]
var body: some View {
VStack {
ForEach(animals, id: \.self) { animal in
Text(animal)
if animals.last != animal {
Divider()
.background(.blue)
}
}
}
}
}
Typically the type inside animals must be Identifiable. In which case the code will be modified as.
if animals.last.id != animal.id {...}
This will avoid any equatable requirements/ implementations
CodePudding user response:
Found a solution!
appending(path:)
seems to only work on key paths erased toAnyKeyPath
.- Then,
appending(path:)
returns an optionalAnyKeyPath?
— this needs to get cast down toKeyPath<(Data.Index, Data.Element), ID>
to satisfy theid
parameter.
struct ForEachDividerView<Data, Content, ID>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
var data: Data
var id: KeyPath<Data.Element, ID>
var content: (Data.Element) -> Content
var body: some View {
let enumerated = Array(zip(data.indices, data))
/// first create a `AnyKeyPath` that extracts the element from `enumerated`
let elementKeyPath: AnyKeyPath = \(Data.Index, Data.Element).1
/// then, append the `id` key path to `elementKeyPath` to extract the `Hashable` property
if let fullKeyPath = elementKeyPath.appending(path: id) as? KeyPath<(Data.Index, Data.Element), ID> {
ForEach(enumerated, id: fullKeyPath) { index, data in
content(data)
if let index = index as? Int, index != enumerated.count - 1 {
Divider()
.background(.blue)
}
}
}
}
}
Usage:
struct Person {
var name: String
}
struct ContentView: View {
let people: [Person] = [
.init(name: "Anna"),
.init(name: "Bob"),
.init(name: "Chris")
]
var body: some View {
VStack {
ForEachDividerView(data: people, id: \.name) { person in
Text(person.name)
}
}
}
}
Result:
CodePudding user response:
Does everything needs to be in a ForEach? If not, you can consider not using indices at all:
struct ForEachDividerView<Data, Content, ID>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
var data: Data
var id: KeyPath<Data.Element, ID>
var content: (Data.Element) -> Content
var body: some View {
if let first = data.first {
content(first)
ForEach(data.dropFirst(), id: id) { element in
Divider()
.background(.blue)
content(element)
}
}
}
}