Create a list of views in SwiftUI using ForEach

⋅ 7 min read ⋅ SwiftUI List Navigation ForEach

Table of Contents

Part 1 in the series "Building Lists and Navigation in SwiftUI". We visit the first building block of any list view, content, and how to create them.

  1. Content View
  2. ScrollView
  3. ListView
  4. NavigationView

List is an essential element in the iOS world. In UIKit, we have many ways to create a list such as UITableView, UICollectionView, and UIScrollView. In this blog post series, I will cover every foundation that you need to know to make a list in SwiftUI.

This first article will talk about the first building block of any list view, content. We will learn different ways of creating content for a list.

Content View

One of the most important things in any list view is content. There are two ways to create content for your list, statically and dynamically.

Static Content

For static content, you declare child views by listing them in the body of the view that supports @ViewBuilder, which is basically every view that conforms to the View protocol.

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {

/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
associatedtype Body : View

/// The content and behavior of the view.
@ViewBuilder var body: Self.Body { get }
}

In the following example, we declare three text view in the body of ContentView.

struct ContentView: View {
var body: some View {
Text("First")
Text("Second")
Text("Third")
}
}

@ViewBuilder will pack all child views into a tuple for a parent to work with. The default behavior of View will simply layout this vertically.

Dynamic Content with ForEach

For a simple layout, static content might be enough, but you would most of the time want something more dynamic, like showing content from an array of information. You can do that with the help of ForEach.

ForEach has many ways to create views from an underlying collection that we will discuss in the next section. Here is an example of views created from a collection of strings.

struct ContentView: View {
let positions = ["First", "Second", "Third"]

var body: some View {
ForEach(positions, id: \.self) { position in
Text(position)
}
}
}

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Loop with ForEach in SwiftUI

What is ForEach

ForEach is a structure that computes views on demand from an underlying collection of identified data. There are three ways to initialize ForEach. Each of them might seem different, but they all shared the same purpose: defining a data and its (Hashable) identifier.

struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable

Loop over a specific number of times

The first initializer allows us to loop over a range of integers.

init(_ data: Range<Int>, content: @escaping (Int) -> Content)

Here is the example using ForEach to loop over a range of 0..<3.

struct ContentView: View {
let positions = ["First", "Second", "Third"]

var body: some View {
ForEach(0..<positions.count) { index in
Text(positions[index])
}
}
}

Data, in this case, is a range of integers. The elements of the range also use as an identifier. This is possible since Int is conforming to Hashable.

ForEach<Range<Int>, Int, Text>

// Data = Range<Int>
// ID = Int
// Content = Text

Loop over any data

The second form creates an instance that creates views from data uniquely identified by the provided key path.

init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)

In the following example, we looped over an array of strings and specified \.self key path, which refers to the string itself as an identifier.

struct ContentView: View {
let positions = ["First", "Second", "Third"]

var body: some View {
ForEach(positions, id: \.self) { position in
Text(position)
}
}
}

Data is an array of strings. The identifier is the string itself, String.

ForEach<[String], String, Text>

// Data = [String]
// ID = String
// Content = Text

Let see one more example when looping over a custom type. We declare a new type, Position, which contains an id of Int and a name of String.

struct Position {
let id: Int
let name: String
}

We can loop over Position like this:

struct ContentView: View {
let positions = [
Position(id: 1, name: "First"),
Position(id: 2, name: "Second"),
Position(id: 3, name: "Third")
]

var body: some View {
ForEach(positions, id: \.id) { position in
Text(position.name)
}
}
}

Data, in this case, is an array of Position. The identifier is the id property, which is Int.

ForEach<[Position], Int, Text>

// Data = [Position]
// ID = Int
// Content = Text

Loop over Identifiable data

The last one is the most compact form of all three, but it required each element in a collection to conform Identifiable protocol.

extension ForEach where ID == Data.Element.ID, Content : View, Data.Element : Identifiable {
public init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content)
}

The protocol only needs an id, which is Hashable.

public protocol Identifiable {

/// A type representing the stable identity of the entity associated with
/// an instance.
associatedtype ID : Hashable

/// The stable identity of the entity associated with this instance.
var id: Self.ID { get }
}

This initializer is quite the same as our previous one. But instead of specified a keypath, it will use an id of the Identifiable element.

To make it work with our Position struct, we have to make our struct conform to the Identifiable protocol.

Since our struct already has an id of type Int which is happened to conform Hashable, this is all we need to do:

struct Position: Identifiable {
let id: Int
let name: String
}

Then we can use it as a ForEach argument like this:

struct ContentView: View {
let positions = [
Position(id: 1, name: "First"),
Position(id: 2, name: "Second"),
Position(id: 3, name: "Third")
]

var body: some View {
ForEach(positions) { position in
Text(position.name)
}
}
}

Data is an array of Position. The identifier is the id of Identifiable protocol, which is Int.

ForEach<[Position], Int, Text>

// Data = [Position]
// ID = Int
// Content = Text

What can go wrong with ID/Identifiable

When using an initializer that accepts identifier, whether in the form of KeyPath<Data.Element, ID> or Identifiable protocol, it's important to make sure that the values are unique. These ID are used to uniquely identifies the element and create views based on these underlying data. Failing to do so might cause unexpected behavior.

There is no compile error when your ID isn't unique. You won't notice it until it's too late, so make sure you use something really unique as an ID.

struct ContentView: View {
let positions = [
Position(id: 1, name: "First"),
Position(id: 2, name: "Second"),
Position(id: 1, name: "Third")
]

var body: some View {
ForEach(positions) { position in
Text(position.name)
}

ForEach(positions, id: \.id) { position in
Text(position.name)
}
}
}

The above example will result in the duplicate rendering of "First" text.

Duplicate ID might result in unexpected behavior
Duplicate ID might result in unexpected behavior

Conclusion

All of the examples above will yield the same result. So which initializers to use is based on your data type.

The result of creating views using ForEach
The result of creating views using ForEach

In this article, we learn different tools to populate content, which is an essential part of any list view. In the next article, we will learn how to use this to make a list in SwiftUI.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Apple Documentation


Read more article about SwiftUI, List, Navigation, ForEach, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
Different ways to check if a string contains another string in Swift

Learn how to check if a string contains another string, numbers, uppercased/lowercased string, or special characters.

Next
What is @Environment in SwiftUI

Learn how SwiftUI shares application settings and preference values across the app.

← Home