For loop in SwiftUI - ForEach
SwiftUI is a declarative framework for constructing user interfaces (UI) for iOS Apps as well as other Apple Platforms. In SwiftUI most parts of the user interface are composed of views nested and arranged to present a cohesive App. It is frequently required to present a number of similar views, and a for or while loop would seem like a good approach. SwiftUI framework cannot handle control flow statements such as For or While to layout views, so the framework provides the ForEach struct.
Swift For Loop
There are a number of ways of writing a For
loop in Swift, either by declaring an
array and iterating over it or by iterating over a range. This example prints out the
value of i
five times and is valid Swift and can be run in a Swift Playground.
Unfortunately, this type of For
loop will not work in SwiftUI because
ViewBuilder cannot handle the control flow statements.
1for i in 1...5 {
2 print("i is \(i)")
3}
The output is
i is 1
i is 2
i is 3
i is 4
i is 5
1 var body: some View {
2 for i in 1...5 {
3 Text("i is \(i)")
4 }
5 }
Closure containing control flow statement cannot be used with result builder 'ViewBuilder'
Closure containing control flow error
ForEach without Identifier
ForEach is a structure in SwiftUI that computes views on demand from a collection of Identifiable data. A common mistake is to rewrite the for loop with a ForEach, but this will result in errors if the collection of items has no identifier. ForEach is not the same as For In Loop in Swift.
1struct ForLoopView: View {
2 var body: some View {
3 VStack {
4 ForEach(1...5) { i in
5 Text("i is \(i)")
6 }
7 Spacer()
8 }
9 }
10}
Collection in ForEach is missing identifier
Cannot convert value of type 'ClosedRange<Int>' to expected argument of 'Range<Int>'
Initially, the error given may not seem very helpful, but switching the range to a half-open range resolves the issue and displays the Text Views in the Vertical Stack.
1struct ForLoopView: View {
2 var body: some View {
3 VStack {
4 ForEach(1..<6) { i in
5 Text("i is \(i)")
6 }
7 Spacer()
8 }
9 }
10}
Using ForEach with half-open range
ForEach with Identifier in SwiftUI
It is possible to use a collection of objects that do not conform to Identifiable
protocol by providing an id parameter in the ForEach
initializer. The id(: \.self)
parameter is used so SwiftUI can identify each element in the collection
uniquely and can know which one is added or deleted.
1struct ForLoopView: View {
2 var body: some View {
3 VStack {
4 ForEach(1...5, id: \.self) { i in
5 Text("i is \(i)")
6 }
7 Spacer()
8 }
9 }
10}
All three of these do the same thing. The variable can be omitted and the first
parameter syntax $0
used to represent the paramater in the loop.
1 ForEach(1..<6) { i in
2 Text("\(i)")
3 }
4
5 ForEach(1..<6) {
6 Text("\($0)")
7 }
8
9
10 ForEach(1...5, id: \.self) { i in
11 Text("\(i)")
12 }
Repeated text views in a vertical stack using foreach
Repeat simple views
It is also possible to use an underscore for the parameter to show that the parameter is not used inside the loop. It is better to do this so Swift does not waste resources creating instances of the variable that are not needed.
1struct StarView: View {
2 var body: some View {
3 VStack {
4 Spacer().frame(height:40)
5 HStack {
6 ForEach(1...5, id:\.self) { _ in
7 Image(systemName: "star.fill")
8 .font(.system(size: 30))
9 .foregroundColor(.yellow)
10 }
11 }
12 Spacer()
13 }
14 }
15}
Repeated star view
Create a Chess Board
A checkered pattern can be created using nested ForEach
loops for rows and columns
and setting the cell fill color based on the current row and column.
1 let cellCount = 8
2
3 ...
4
5 VStack(spacing:3) {
6 ForEach(1...cellCount, id:\.self) { r in
7 HStack(spacing:3) {
8 ForEach(1...cellCount, id:\.self) { c in
9 ZStack {
10 Rectangle()
11 .fill(((r+c)%2 == 0) ? Color(#colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1)) : Color(#colorLiteral(red: 0.3098039329, green: 0.2039215714, blue: 0.03921568766, alpha: 1)))
12 .frame(width: 40, height: 40)
13 }
14 }
15 }
16 }
17 }
Chess board created with nested loops
Other ForEach examples
ForEach can be used with collections of anything, here are examples of numbers, strings and colors.
1 var body: some View {
2 VStack(spacing:20) {
3 HStack {
4 ForEach(1...5, id: \.self) {
5 Text("i is \($0)")
6 }
7 }
8 HStack {
9 ForEach([21,42,2,7,32,15], id: \.self) {
10 Text("\($0)")
11 }
12 }
13 HStack {
14 ForEach(["red","green","blue"], id: \.self) {
15 Text("\($0)")
16 .frame(width:50)
17 }
18 }
19 HStack {
20 ForEach([Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple], id: \.self) {
21 Ellipse()
22 .fill($0)
23 .frame(width: 30, height: 50)
24 }
25 }
26 Spacer()
27 }
28 }
Examples of ForEach with numbers, strings and colors
ForEach with collection of objects with identifiers
ForEach can be used with collections of objects that conform to the Identifiable
protocol. Here a ColorItem
struct is defined that contains properties of a color
and has a unique identifier property id
. A rainbow
is defined as an array of
colorItems and this is used in the RainbowView
to display a view for each of the
colors using the ForEach structure.
1struct ColorItem: Identifiable {
2 let id = UUID()
3 let name: String
4 let col: Color
5 let wavelength: String
6 let hex: String
7}
1let rainbow = [
2 ColorItem(name: "Red",
3 col: Color(.red),
4 wavelength: "625-740",
5 hex: "#ff0000"),
6 ColorItem(name: "Orange",
7 col: Color(.orange),
8 wavelength: "585-620",
9 hex: "#ffa500"),
10 ColorItem(name: "Yellow",
11 col: .yellow,
12 wavelength: "570-590",
13 hex: "#ffff00"),
14 ColorItem(name: "Green",
15 col: .green,
16 wavelength: "495-570",
17 hex: "#008000"),
18 ColorItem(name: "Blue",
19 col: .blue,
20 wavelength: "450-495",
21 hex: "#0000ff"),
22 ColorItem(name: "Indigo",
23 col: Color(red: 75/255, green: 0/255, blue: 130/255),
24 wavelength: "445-464",
25 hex: "#4b0082"),
26 ColorItem(name: "Violet",
27 col: Color(red: 238/255, green: 130/255, blue: 238/255),
28 wavelength: "380-450",
29 hex: "#ee82ee")
30]
1struct RainbowView: View {
2 var body: some View {
3 VStack {
4 ForEach(rainbow) { c in
5 HStack {
6 Rectangle()
7 .fill(c.col)
8 .frame(width: 30, height: 30)
9 Text(c.name)
10 .frame(width:100, alignment:.leading)
11 Text(c.wavelength)
12 Spacer()
13 }
14 .padding(.horizontal)
15 }
16
17 Spacer().frame(height:100)
18
19 ZStack {
20 ForEach(rainbow.indices) { i in
21 Circle()
22 .trim(from: 0, to: 0.5)
23 .stroke(
24 rainbow[i].col,
25 style: StrokeStyle(lineWidth: 10))
26 .frame(width: 300, height: 300, alignment: .center)
27 .rotationEffect(Angle(degrees: -180))
28 .scaleEffect(x: CGFloat(20 - i)/20.0,
29 y: CGFloat(20 - i)/20.0)
30 }
31 }
32 Spacer()
33 }
34 }
35}
Stacks of views created from collection with identifiers
Conclusion
It is not possible to use procedural looping mechanisms such as For and While loops to layout views in SwiftUI. The SwiftUI framework provides the ForEach struct to accomplish an alternative to repeatedly add views to a view. A requirement of ForEach is to use a collection of objects that conform to Identifiable protocol. This can be worked around using the self parameter so the object can be uniquely identified.