Using SwiftUI’s ForEach with raw values | Swift by Sundell

생성일
Feb 24, 2020 10:52 AM
언어
Swift
분야
URL
 
notion imagenotion image
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) } } } }
Crash reporting, bug reporting, and customizable in-app surveys — all in one SDK. Get to know exactly which line of code that caused each crash, along with network logs, and detailed reproduction steps, all presented within a powerful Session Profiler — to help you identify and resolve severe crashes quickly.