Create a list of views in SwiftUI using ForEach
source link: https://sarunw.com/posts/create-list-of-views-in-swiftui-using-foreach/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Create a list of views in SwiftUI using ForEach
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.
Before Xcode 12, this is not the case. So, if you use an old version of Xcode, the above statement might not apply to you.
@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.
We won't go into detail about @ViewBuilder
in this article.
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)
}
}
}
Learn everything you need to know about Sign in with Apple to be able to integrate it in your existing app or a new one.
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
This variation is suitable when you have a collection of primitive types.
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 behaviorPlease don't rely on the unknown implementation detail about the rendering order of duplicate ID. In the future version, "Third" might be print.
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 ForEachIn 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.
Learn everything you need to know about Sign in with Apple to be able to integrate it in your existing app or a new one.
Related Resources
Apple Documentation
Get new posts weekly
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 — entirely for free.
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.
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.
What is @Environment in SwiftUI
Learn how SwiftUI shares application settings and preference values across the app.
This modifier can interpolate position and size between two views. This is one of the most exciting features for me. Let's see what is capable of in this beta.
SwiftUIIn iOS 14, we have a new way to put images along with texts.
SwiftUIMost complex custom views can be made by composing many basic shapes together. Today we will learn basic operations that we can do with them. It may seem trivial, but knowing these basics will benefit you in the future.
SwiftUIRecommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK