Looping views SwiftUI

Hello
Is there a view to loop different views, for example in a ForEach loop

Here is what I mean:

Code Block
import SwiftUI
struct ContentView: View {
@State var view1 = false
@State var view2 = false
@State var view3 = false
@State var view4 = false
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
var textSize: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 48
}
return 23
}
var title: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 60
}
return 34
}
var height: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.15
}
return 0.15
}
var weight: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.44
}
return 0.43
}
var body: some View {
NavigationView{
GeometryReader { geometry in
ScrollView(.vertical, showsIndicators: true) {
LazyVGrid(columns: gridItemLayout, spacing: 18){
Group{
Text("View1")
.foregroundColor(.black)
.frame(width: geometry.size.width * weight, height: geometry.size.height * height)
.background(Color.white)
.onTapGesture {
view1 = true
}
.sheet(isPresented: $view1) {
View1()
}
Text("View2")
.foregroundColor(.black)
.frame(width: geometry.size.width * weight, height: geometry.size.height * height)
.background(Color.white)
.onTapGesture {
view2 = true
}
.sheet(isPresented: $view2) {
View2()
}
Text("View3")
.foregroundColor(.black)
.frame(width: geometry.size.width * weight, height: geometry.size.height * height)
.background(Color.white)
.onTapGesture {
view3 = true
}
.sheet(isPresented: $view3) {
View3()
}
Text("View4")
.foregroundColor(.black)
.frame(width: geometry.size.width * weight, height: geometry.size.height * height)
.background(Color.white)
.onTapGesture {
view4 = true
}
.sheet(isPresented: $view4) {
View4()
}
}
}.padding()
}
}
.navigationTitle("Title")
}
}
}

Is there a way to put the 4 views in a loop instead of making four different Text Views(they are different views)

Thank your for your time

Answered by Claude31 in 654478022
Would this do it ?

Code Block
import SwiftUI
struct ContentView: View {
@State var view1 = false
@State var view2 = false
@State var view3 = false
@State var view4 = false
@State var views = [false, false, false, false]
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
private var viewTexts = ["View1", "View2", "View3", "View4"]
var textSize: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 48
}
return 23
}
var title: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 60
}
return 34
}
var height: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.15
}
return 0.15
}
var weight: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.44
}
return 0.43
}
var body: some View {
NavigationView{
GeometryReader { geometry in
ScrollView(.vertical, showsIndicators: true) {
LazyVGrid(columns: gridItemLayout, spacing: 18) {
Group {
ForEach(0..<views.count) { i in
Text(viewTexts[i])
.foregroundColor(.black)
.frame(width: geometry.size.width * weight, height: geometry.size.height * height)
.background(Color.white)
.onTapGesture {
views[i] = true
}
.sheet(isPresented: $views[i]) {
switch i {
case 0: View1()
case 1: View2()
case 2: View3()
case 3: View4()
default: View1()
}
}
}
}
}.padding()
}
}
.navigationTitle("Title")
}
}
}
struct View1: View {
var body: some View {
Text("View1")
}
}
struct View2: View {
var body: some View {
Text("View2")
}
}
struct View3: View {
var body: some View {
Text("View3")
}
}
struct View4: View {
var body: some View {
Text("View4")
}
}


You can go one step further

Declare

Code Block
let Views : [AnyView] = [AnyView(View1()), AnyView(View2()), AnyView(View3()), AnyView(View4())]


and replace the switch lines 57 to 63 by:
Code Block
.sheet(isPresented: $views[i]) {
Views[i]
}

Accepted Answer
Would this do it ?

Code Block
import SwiftUI
struct ContentView: View {
@State var view1 = false
@State var view2 = false
@State var view3 = false
@State var view4 = false
@State var views = [false, false, false, false]
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
private var viewTexts = ["View1", "View2", "View3", "View4"]
var textSize: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 48
}
return 23
}
var title: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 60
}
return 34
}
var height: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.15
}
return 0.15
}
var weight: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.44
}
return 0.43
}
var body: some View {
NavigationView{
GeometryReader { geometry in
ScrollView(.vertical, showsIndicators: true) {
LazyVGrid(columns: gridItemLayout, spacing: 18) {
Group {
ForEach(0..<views.count) { i in
Text(viewTexts[i])
.foregroundColor(.black)
.frame(width: geometry.size.width * weight, height: geometry.size.height * height)
.background(Color.white)
.onTapGesture {
views[i] = true
}
.sheet(isPresented: $views[i]) {
switch i {
case 0: View1()
case 1: View2()
case 2: View3()
case 3: View4()
default: View1()
}
}
}
}
}.padding()
}
}
.navigationTitle("Title")
}
}
}
struct View1: View {
var body: some View {
Text("View1")
}
}
struct View2: View {
var body: some View {
Text("View2")
}
}
struct View3: View {
var body: some View {
Text("View3")
}
}
struct View4: View {
var body: some View {
Text("View4")
}
}


You can go one step further

Declare

Code Block
let Views : [AnyView] = [AnyView(View1()), AnyView(View2()), AnyView(View3()), AnyView(View4())]


and replace the switch lines 57 to 63 by:
Code Block
.sheet(isPresented: $views[i]) {
Views[i]
}

So, the code is now refactored:

Code Block
import SwiftUI
struct ContentView: View {
@State var views = [false, false, false, false]
let Views : [AnyView] = [AnyView(View1()), AnyView(View2()), AnyView(View3()), AnyView(View4())]
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
private var viewTexts = ["View1", "View2", "View3", "View4"]
var textSize: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 48
}
return 23
}
var title: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 60
}
return 34
}
var height: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.15
}
return 0.15
}
var weight: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.44
}
return 0.43
}
var body: some View {
NavigationView{
GeometryReader { geometry in
ScrollView(.vertical, showsIndicators: true) {
LazyVGrid(columns: gridItemLayout, spacing: 18) {
Group {
ForEach(0..<views.count) { i in
Text(viewTexts[i])
.foregroundColor(.black)
.frame(width: geometry.size.width * weight, height: geometry.size.height * height)
.background(Color.white)
.onTapGesture {
views[i] = true
}
.sheet(isPresented: $views[i]) {
Views[i]
}
}
}
}.padding()
}
}
.navigationTitle("Title")
}
}
}
struct View1: View {
var body: some View {
Text("View1")
}
}
struct View2: View {
var body: some View {
Text("View2")
}
}
struct View3: View {
var body: some View {
Text("View3")
}
}
struct View4: View {
var body: some View {
Text("View4")
}
}

Rather than try to define all of your views upfront, define the models that power them. You can then easily iterate through your model to generate the desired views. Additionally, rather than create separate state for each possible sheet that can be presented, there is an alternate sheet modifier that accepts a binding to an Identifiable instance. This drastically reduces reduces boilerplate, and possible sources of bugs.

Finally AnyView should only ever be a last resort, as it removes the compiler and SwiftUI's ability to optimize possibly destroying performance. If you find yourself using AnyView there is almost always a better way.

Code Block
struct Model: Identifiable {
var id: String { title }
let title: String
}
struct DetailView: View {
let title: String
var body: some View {
Text(title)
}
}
struct ContentView: View {
@State private var selectedItem: Model?
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
private let items = [
Model(title: "View1"),
Model(title: "View2"),
Model(title: "View3"),
Model(title: "View4")
]
var textSize: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 48
}
return 23
}
var title: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 60
}
return 34
}
var height: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.15
}
return 0.15
}
var weight: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.44
}
return 0.43
}
var body: some View {
NavigationView {
GeometryReader { geometry in
ScrollView(.vertical, showsIndicators: true) {
LazyVGrid(columns: gridItemLayout, spacing: 18) {
ForEach(items) { item in
Text(item.title)
.foregroundColor(.black)
.frame(width: geometry.size.width * weight, height: geometry.size.height * height)
.background(Color.white)
.onTapGesture {
selectItem(item)
}
}
}
.padding()
}
}
.navigationTitle("Title")
}
.sheet(item: $selectedItem, content: view(for:))
}
private func selectItem(_ item: Model) {
withAnimation { self.selectedItem = item }
}
@ViewBuilder
private func view(for item: Model) -> some View {
DetailView(title: item.title)
// switch item.id {
// case "View1": View1()
// case "View2": View2()
// case "View3": View3()
// case "View4": View4()
// default: EmptyView()
// }
}
}

Separating out the details into smaller pieces allows customization and control through one source of truth as stressed in WWDC presentation of SwiftUI. This makes more of this reusable, and lets you start to understand whether you really want to customize so many things in so many places, or just have one place for that to be done.

Below, DisplayedView, is what you want on your ActionSheet. Keeping that as one "name" simplifies things. But, to customize that sheet view, you can still either replace DisplayedView with the specific sheets you need, or provide a parameter to it that further nests your customization so that you can pass in an array of model objects as jjatie showed, and then just have DisplayedView manage that. This allows you to use DisplayedView elsewhere with your model instances and not have it all tied up in nested content around your navigation view.

Code Block
class GeometryData {
var textSize: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 48
}
return 23
}
var title: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 60
}
return 34
}
var height: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.15
}
return 0.15
}
var weight: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return 0.44
}
return 0.43
}
}
struct BaseView : View {
var text : String
var geometry : GeometryProxy
var sizes : GeometryData = GeometryData()
@State var active : Bool = false
var showView : DisplayedView
var body: some View {
Text(text)
.foregroundColor(.black)
.frame(width: geometry.size.width * sizes.weight, height: geometry.size.height * sizes.height)
.background(Color.white)
.onTapGesture {
active = true
}
.sheet(isPresented: $active) {
showView
}
}
}
struct DisplayedView : View {
var which : Int
var body: some View {
Text("This is the \(which) view")
}
}
struct ContentView: View {
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
let viewCount = 4
var body: some View {
NavigationView{
GeometryReader { geometry in
ScrollView(.vertical, showsIndicators: true) {
LazyVGrid(columns: gridItemLayout, spacing: 18){
Group{
ForEach(0..<viewCount) { item in
BaseView(text:"View\(item+1)", geometry:geometry, showView: DisplayedView(which:item+1))
}
}
}.padding()
}
}
.navigationTitle("Title")
}
}
}

Looping views SwiftUI
 
 
Q