For loop in SwiftUI - ForEach

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.