SwiftUI View does not updated when ObservedObject changed

眉间皱痕 提交于 2021-02-19 04:01:51

问题


I have a content view where i'm showing a list of items using ForEach

@ObservedObject var homeVM : HomeViewModel

var body: some View {
    ScrollView (.horizontal, showsIndicators: false) {
        HStack(spacing: 10) {
            Spacer().frame(width:8)
            ForEach(homeVM.favoriteStores , id: \._id){ item in
                StoreRowOneView(storeVM: item)
            }
            Spacer().frame(width:8)
        }
    }.frame(height: 100)
}

And my ObservableObject contains

@Published var favoriteStores = [StoreViewModel]()

And StoreViewModel is defined as below

class StoreViewModel  : ObservableObject {
    @Published var store:ItemStore

    init(store:ItemStore) {
        self.store = store
    }

    var _id:String {
        return store._id!
    }
}

If i fill the array directly it work fine , and i can see the list of stores but if i filled the array after a network call (background job) , the view did not get notified that there is a change

 StoreApi().getFavedStores(userId: userId) { (response) in
            guard response != nil else {
                self.errorMsg = "Something wrong"
                self.error = true
                return
            }
            self.favoriteStores = (response)!.map(StoreViewModel.init)
            print("counnt favorite \(self.favoriteStores.count)")
        }

But the weird thing is : - if i add a text which show the count of the array items , the view will get notified and everything work fine

That's what i mean :

    @ObservedObject var homeVM : HomeViewModel
    var body: some View {
        ScrollView (.horizontal, showsIndicators: false) {
            HStack(spacing: 10) {
                Spacer().frame(width:8)
                ForEach(homeVM.favoriteStores , id: \._id){ item in
                    StoreRowOneView(storeVM: item)
                }
                Text("\(self.homeVM.favoriteStores.count)").frame(width: 100, height: 100)
                Spacer().frame(width:8)
            }
        }.frame(height: 100)
    }

Any explaining for that ?


回答1:


For horizontal ScrollView you need to have your height fixed or just use conditional ScrollView which will show when someModels.count > 0

var body: some View {
   ScrollView(.horizontal) {
      HStack() {
         ForEach(someModels, id: \.someParameter1) { someModel in
            someView(someModel)
         }
      }.frame(height: someFixedHeight)
   }
}
// or
var body: some View {
   VStack {
      if someModels.count > 0 {
         ScrollView(.horizontal) {
            HStack() {
               ForEach(someModels, id: \.someParameter1) { someModel in
                  someView(someModel)
               }
            }
         }
      } else {
        EmptyView()
        // or other view
      }
   }
}

Properly conforming to Hashable protocol by implementing

func hash(into hasher: inout Hasher) { 
   hasher.combine(someParameter1)
   hasher.combine(someParameter2)
}

and

static func == (lhs: SomeModel, rhs: SomeModel) -> Bool {
   lhs.someParameter1 == rls.someParameter1 && lhs.someParameter2 == rls.someParameter2

}

And also following @Asperi suggestion of using proper id from model, which must be unique to every element.

ForEach(someModels, id: \.someParameter1) { someModel in
    someView(someModel)
}

There is no other way for ForEach to notify updates other than unique ids.

Text(someModels.count) was working because it is notifying the changes as count is being changed.




回答2:


Share some experiences that might help.

When I used ScrollView + ForEach + fetch, nothing shows until I trigger view refresh via other means. It shows normally when I supply fixed data without fetching. I solved it by specifying .infinity width for ScrollView.

My theory is that ForEach re-renders when fetch completes, but ScrollView somehow does not adjust its width to fit the content.

It is similar in your case. By supplying fixed width Text, ForEach can calculate its width during init, and ScrollView can use that to adjust its width. But in case of fetch, ForEach has initial 0 content width, and so is ScrollView. When fetch completes ScrollView does not update its width accordingly.

Giving ScrollView some initial width should solve your problem.




回答3:


I would start from forwarding results to the main queue, as below

DispatchQueue.main.async {
    self.favoriteStores = (response)!.map(StoreViewModel.init)
}

Next... ForEach tracks id parameters, so you should be sure that results have different _id s in the following, otherwise ForEach is not marked as needed to update

ForEach(homeVM.favoriteStores , id: \._id){ item in
    StoreRowOneView(storeVM: item)
}



回答4:


What is ItemStore? more importantly

ForEach(homeVM.favoriteStores , id: \._id){ item in
    StoreRowOneView(storeVM: item)
}

what is _id i believe your _id is same each time so it will not update the list. You have to put id: some variable that you want to re-render the list when it changed.

Update: If you do not have any other variable to take its value as reference than you can use

ForEach((0..<homeVM.favoriteStores.count)) { index in
    StoreRowOneView(storeVM:: homeVM.favoriteStores[index])
}


来源:https://stackoverflow.com/questions/59754160/swiftui-view-does-not-updated-when-observedobject-changed

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!