Using SwiftUI’s ForEach with raw values
Discover page available: SwiftUISwiftUI’s ForEach
type enables us to create a series of views by transforming each element within a collection. However, since ForEach
reuses the views that it creates in order to optimize performance (just like other list-based views, like UITableView
and UICollectionView
, do), it requires us to provide a way to identify each of the elements that we’re basing its views on.
When the elements that we’re transforming conform to the Identifiable
protocol, that kind of identification is taken care of automatically, and we can simply pass our collection of values directly to ForEach
. For example, here we’re transforming an array of User
values into a list of vertically arranged UserView
instances:
struct User: Identifiable {
let id: UUID
var name: String
}
struct UserList: View {
var users = [User]()
var body: some View {
VStack {
ForEach(users) { user in
UserView(user: user)
}
}
}
}
Note how we could’ve written the above ForEach
expression as ForEach(users, content: UserView.init)
, since Swift supports first class functions.
However, sometimes we might want to base a ForEach
on a collection of simpler, raw values — such as strings. Doing that might initially seem difficult, since we wouldn’t want to make String
unconditionally conform to Identifiable
. Thankfully, there’s a way to make that happen, by using the \.self
key path to compute each element’s identifier — like this:
struct TagList: View {
var tags: [String]
var body: some View {
HStack {
// Using '\.self', we can refer to each element directly,
// and use the element's own value as its identifier:
ForEach(tags, id: \.self) { tag in
Text(tag)
.padding(3)
.background(Color.secondary)
.cornerRadius(5)
}
}
}
}
Another option would of course be to wrap our raw values using an Identifiable
type, for example like this:
struct Tag: Identifiable {
var id: String { name }
var name: String
}
struct TagList: View {
var tags: [Tag]
var body: some View {
HStack {
ForEach(tags) { tag in
Text(tag.name)
.padding(3)
.background(Color.secondary)
.cornerRadius(5)
}
}
}
}
When using either of the above two techniques, it’s important to first make sure that the values that we’re dealing with are all unique (at least within that collection), since otherwise ForEach
might incorrectly reuse the resulting views.
Finally, if we plan to use ForEach
with raw values in several places throughout our code base, we might want to create a simple convenience API for doing that, to avoid having to repeat that \.self
key path argument within all those places:
extension ForEach where Data.Element: Hashable, ID == Data.Element, Content: View {
init(values: Data, content: @escaping (Data.Element) -> Content) {
self.init(values, id: \.self, content: content)
}
}
Note how we use an external parameter label above, which the built-in ForEach
APIs don’t. That’s very much by design, to avoid collisions, for example when using Range<Int>
, which ForEach
natively supports.
With the above extension in place we can now easily pass any collection of raw values, such as strings and integers, to ForEach
— like this:
struct TagList: View {
var tags: [String]
var body: some View {
HStack {
ForEach(values: tags) { tag in
Text(tag)
.padding(3)
.background(Color.secondary)
.cornerRadius(5)
}
}
}
}