Create a list of views in SwiftUI using 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.
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.
Debug 10x faster with Proxyman: Your ultimate tool to capture HTTPs requests/ responses, natively built for your iPhone and macOS. Special deal for Black Friday: Get 30% off for all Proxyman licenses with code “BLACKFRIDAY2024”.
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.
Conclusion
All of the examples above will yield the same result. So which initializers to use is based on your data type.
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.
Debug 10x faster with Proxyman: Your ultimate tool to capture HTTPs requests/ responses, natively built for your iPhone and macOS. Special deal for Black Friday: Get 30% off for all Proxyman licenses with code “BLACKFRIDAY2024”.
Related Resources
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 ShareDifferent 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.
What is @Environment in SwiftUI
Learn how SwiftUI shares application settings and preference values across the app.