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
?
4
Answers
Found a solution!
appending(path:)
seems to only work on key paths erased toAnyKeyPath
.appending(path:)
returns an optionalAnyKeyPath?
— this needs to get cast down toKeyPath<(Data.Index, Data.Element), ID>
to satisfy theid
parameter.Usage:
Result:
Simple way
Typically the type inside animals must be Identifiable. In which case the code will be modified as.
This will avoid any equatable requirements/ implementations
Does everything need to be in a ForEach? If not, you can consider not using indices at all:
Using the article mentioned in a comment I built the following. It takes the set of views and places a divider between them.
This is also useful when the views are not being generated by a
ForEach
, especially when one or more of the views is removed conditionally (e.g. using anif
statement).