2

Create a list of views in SwiftUI using ForEach

 3 years ago
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.

  1. Content View

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)
}
}
}
Practical Sign in with Apple

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.

Get it now

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 behaviorDuplicate ID might result in unexpected behavior

Please 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 ForEachThe 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.

Practical Sign in with Apple

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.

Get it now

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.

Become a patron

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.

Related Posts

Browse all content by tag

A first look at matchedGeometryEffect

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.

SwiftUI
How to Add inline images with text in SwiftUI

In iOS 14, we have a new way to put images along with texts.

SwiftUI
SwiftUI basic Shape operations

Most 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.

SwiftUI

← Home


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK