WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

SwiftUI ForEach Duplicate prints

Forums > SwiftUI

Edited: First time posting. In my application I am creating various arrays of Identifiable structs using my API response. I am iterating over said array to build lists within the Content and Sidebar columns of my Navigation Split View. If I print the array before my ForEach call, the array is normal. When printing each item from within the ForEach (let _ = print(item)) the item prints twice. However the item is only added to the List once. Is this normal behavior? It appears to be happening with all of my ForEach calls. Visually the view looks correct, just want to be sure there isn’t any additional looping or view updated occurring.

Thanks in advanced

Printing each item of array. Resulting in duplicate prints.

//
    //  TeamView.swift
    //  myDashboard
    //
    //  Created by nl492k on 10/18/22.
    //

    import SwiftUI

    struct TeamView: View {
        var user: loggedInUser
        var viewData = apiData()

    // viewData is an instance of the apiData struct that includes 2 Arrays of identifieable structs ("gauges" & "trends") and a "team" struct that containts an array of idenfifiable structs "teamMembers"  viewData is a singular object that is updated by the completion handler of my API call.

    //    struct apiData {
    //        var gauges : Array<gaugeObj>
    //        var trends : Array<trendObj>
    //        var team : teamObj
    //
    //        init(gauges : Array<gaugeObj> = Array<gaugeObj>(), trends: Array<trendObj> = Array<trendObj>(), team: teamObj = teamObj()) {
    //            self.gauges = gauges
    //            self.trends = trends
    //            self.team = team
    //        }
    //    }

        @Binding var uid_selection: String?
        var emulation_uid: String

        var body: some View {
            if viewData.team.attuid == "" {
                Label("Not Signed In", systemImage: "person.crop.circle.fill.badge.questionmark")
            }
            else {
                List(selection: $uid_selection){
                        HStack {
                            NavigationLink(value: viewData.team.superv) {
                                AsyncImage(url: URL(string: "\(userImageUrl)\(viewData.team.attuid)")) { image in
                                    image.resizable()
                                        .clipShape(Circle())
                                        .shadow(radius: 10)
                                        .overlay(Circle().stroke(Color.gray, lineWidth: 2))
                                } placeholder: {
                                    ProgressView()
                                }
                                .frame(width:30, height: 35)
                                VStack (alignment: .leading){
                                    Text("\(viewData.team.fName) \(viewData.team.lName)")
                                    Text("\(viewData.team.jobTitle)")
                                        .font(.system(size: 10, weight: .thin))
                                }
                            }
                            Label("", systemImage:"arrow.up.and.person.rectangle.portrait")
                        }
                    Divider()
                    //------ This prints the Array of identifiable structs, as expected, with no issues --------
                    let _ = print(viewData.team.teamMembers)
                    ForEach(viewData.team.teamMembers) { employee in
                        //----- This prints multiple times per employee in array ------.
                        let _ = print(employee)
                        NavigationLink(value: employee.attuid) {
                            AsyncImage(url: URL(string: "\(userImageUrl)\(employee.attuid)")) { image in
                                image.resizable()
                                    .clipShape(Circle())
                                    .shadow(radius: 10)
                                    .overlay(Circle().stroke(Color.gray, lineWidth: 2))
                            } placeholder: {
                                ProgressView()
                            }
                            .frame(width:30, height: 35)
                            VStack (alignment: .leading){
                                Text("\(employee.fName) \(employee.lName)")
                                Text("\(employee.jobTitle)")
                                    .font(.system(size: 10, weight: .thin))
                            }
                        }
                    }
                }
                .background(Color("ContentColumn"))
                .scrollContentBackground(.hidden)
            }
        }
    }

    struct TeamView_Previews: PreviewProvider {
        static var previews: some View {
            TeamView(user: loggedInUser.shared,
                     viewData: apiData(gauges:gaugesTest,
                                       trends: trendsTest,
                                       team: teamTest),
                     uid_selection: .constant(loggedInUser.shared.attuid),
                     emulation_uid: "")
        }
    }

2      

I've never seen let _ = print() before, but I just made a simple View to try to replicate your problem, and it looks like I see the same result here...

struct ContentView: View {
    var pets = ["cat", "dog", "fish", "rabbit", "lizard", "mouse", "bird"]

    var body: some View {
        List {
            ForEach(pets, id: \.self) { pet in
                let _ =  print(pet)
                Text(pet)
            }
        }
    }
}

Unfortunately, I don't know the reason it is happening, and my searches online haven't come up with anything on this yet.

But I don't think it is anything that you have done wrong with your code. It just seems to have something to do with the way ForEach works.

I don't think that they ever intended for you to be able to include non-View conforming code in the ForEach content, and this way of printing within a closure that is supposed to conform to View seems kind of like a bit of a hack that somebody figured out.

2      

@FlyOstrich takes a guess:

It just seems to have something to do with the way ForEach works.

I think coming from a background with "traditional" languages like C++, Java, Python, etc we tend to think of ForEach as a loop structure that (here's the important part) loops through each object one at a time.

Instead, in a view struct, I like to think of ForEach as a view factory.

See -> View Factory
or See -> View Factory
or See -> View Factory

Take a look at @Fly's simple example. The goal is to create a List containing seven Text() objects. The ForEach statement is a factory that makes Text()views. In the end it makes seven of them that have to be equally lined up on screen inside of the List structure.

Is there enough space? Is the screen wide enough? Does the view have to make room for a NavigationTitle? Is it necessary to adjust the Spacer() ?

There are many calculations to consider. Consequently, SwiftUI may create and destroy subviews several times before settling in on a final presentation view. This is the power of structs in SwiftUI. They are very light weight, and SwiftUI may call them several times with different parameters whilst the layout engine tries to balance all the competing demands.

So it's not surprising that you'll see your print statment more than once in the console. What may be more surprising is that you only see the print statement twice! It might be fun to experiment with the ForEach view factory. Instead of presenting views of the same size, perhaps try a row view that has a random height built in. Then test to see how many times the layout engine asks a view to draw itself before being presented.

This is partly a guess, and partly gleaned from other tutorials. Honestly, I'm not sure either.

2      

@Obelix tries an experiment:

I experimented with @Fly's code. Added random height.

struct RandomHeightPetView: View {
    var pets = ["cat", "dog", "fish", "rabbit", "lizard", "mouse", "bird"]

    var randomHeight: CGFloat { CGFloat.random(in: 25...175) }    // Pick random height
    var randomEye: String { Bool.random() ? "eye.fill" : "eye" }  // Pick random SF Symbol

    var body: some View {
        List {
            ForEach(pets, id: \.self) { pet in
                HStack {
                    let _ = print(pet)
                    Image(systemName: randomEye)
                    Text(pet)
                }.frame(height: randomHeight) // each row is random height
            }
        }
    }
}

The print() still prints each string in the array twice. Not sure what I was expecting. But, here you go.

2      

I am fascinated by this problem. I ran @Fly0strich's and @Obelix's code and they both print just once for me.

I saw your question on Stack and the answer was it's normal behavior. That made me even more curious, why mine prints once. It won't answer my question, but i was wondering if it might show the body rendering twice using let _ = Self._printChanges().

2      

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.