Weekly Swift articles, podcasts and tips by John Sundell.

Using SwiftUI’s ForEach with raw values

Published on 21 Feb 2020
Basics article available: SwiftUI

SwiftUI’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)
            }
        }
    }
}