Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
211 views
in Technique[技术] by (71.8m points)

swift - Problem with updating ForEach with changing number of items - .count seems to return wrong value

I created a simple view designed to show the content of an array that is stored in an Observable Object property:

struct Satellite {
    let name: String
    let orbit: Double
    let color: Color
}

class OrbitalSystem: ObservableObject{
    @Published var objects: [Satellite] =  []
}


struct OrbitalSystemView: View {
    
    @ObservedObject var orbitalSystem: OrbitalSystem
    
    var body: some View{

        VStack{
            ForEach(0..<orbitalSystem.objects.count){ n in
                HStack{
                    Text("(orbitalSystem.objects[n].name)")
                    Spacer()
                    Text("(orbitalSystem.objects[n].orbit, specifier: "%.2f")")
                }
                .padding()
                .background(orbitalSystem.objects[n].color)
            }.padding()
        }
    }
}

Then I want to use this general view in ContentView, however in the App I'm using a different struct for the data (struct Planet) than the one used in the general view (struct Satellite). In order to keep the required changes minimal I added a computed property to the struct Planet converting the content of Planet to an element Satellite. With any change to my array of Planets in ContentView a property observer converts it to an array of Satellites and stores it in the ObservedObject property. To update the view when initially loaded the Array of Planets is set by using the .onAppear modifier.

struct Planet {
    let name: String
    let distanceToSun: Int
    let surface: Color
    
    var satellite : Satellite {
        Satellite(name: self.name, orbit: Double(self.distanceToSun), color: self.surface)
    }
}


struct ContentView: View {
    @ObservedObject var planetarySystem = OrbitalSystem()
    
    @State private var planets: [Planet] = []{
        didSet{
            var objects = [Satellite]()
            for planet in planets {
                objects.append(planet.satellite)
            }
            planetarySystem.objects = objects
        }
    }
    
    var body: some View {
            OrbitalSystemView(orbitalSystem: planetarySystem)
                .onAppear{
                    planets = [
                        Planet(name: "Mercury", distanceToSun: 1, surface: Color.orange),
                        Planet(name: "Venus", distanceToSun: 2, surface: Color.gray),
                        Planet(name: "Earth", distanceToSun: 3, surface: Color.blue),
                        Planet(name: "Mars", distanceToSun: 4, surface: Color.red)
                    ]
                }
    }
}  

This code works in principle but has a very strange behavior. My expected result would be: A Stack of four lines listing: Mercury 1.00, Venus 2.00, Earth 3.00, Mars 4.00 with different colors in the background

however with the code above I get an empty view. I get the expected result by modifying the the OrbitalSystem Class like this:

class OrbitalSystem: ObservableObject{
    @Published var objects: [Satellite] =  [
        Satellite(name: "", orbit: 0, color: Color.white),
        Satellite(name: "", orbit: 0, color: Color.white),
        Satellite(name: "", orbit: 0, color: Color.white),
        Satellite(name: "", orbit: 0, color: Color.white)
    ]
} 

if

@Published var objects: [Satellite] =  [
            Satellite(name: "", orbit: 0, color: Color.white),
            Satellite(name: "", orbit: 0, color: Color.white)
            ]

is changed to this the resulting view also shows only two rows: Mercury 1.00 and Venus 2.00 with the respective background colors.

It therefore seems the .count in the ForEach(0..<orbitalSystem.objects.count) returns the wrong value even though the content of the array has been updated.

I'm using Xcode 12.3

I'd be grateful for any clarification on this behavior. Thanks


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

If your data is dynamic (will change while the view is on screen) you need to give each item an ID so that the view knows when to update.

METHOD 1: You can give it a local ID by just changing your ForEach loop:

        VStack{
            ForEach(orbitalSystem.objects.indices, id: .self) { n in
                HStack{
                    Text("(orbitalSystem.objects[n].name)")
                    Spacer()
                    Text("(orbitalSystem.objects[n].orbit, specifier: "%.2f")")
                }
                .padding()
                .background(orbitalSystem.objects[n].color)
            }.padding()
        }

METHOD 2: You can make Satellite conform to Identifiable and give each item it's own ID when you init() the item. Here you could customize the ID if needed and the ForEach doesn't need to loop on the index, but rather the object itself.

  1. Make Satellite conform to Identifiable.

    struct Satellite: Identifiable {
        let id = UUID()
        let name: String
        let orbit: Double
        let color: Color
    }
    
  2. Update for each to loop

             VStack{
                 ForEach(orbitalSystem.objects) { object in
                     HStack{
                         Text("(object.name)")
                         Spacer()
                         Text("(object.orbit, specifier: "%.2f")")
                     }
                     .padding()
                     .background(object.color)
                 }.padding()
             }
    

If you take a look at the different init methods of the ForEach, you'll notice that the Range<Int> method computes views "on demand over a given constant range." So, you need to use one of the other inits for dynamic data.

enter image description here


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share

2.1m questions

2.1m answers

62 comments

56.6k users

...