Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI View does not updated when ObservedObject changed

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 ?

like image 826
Ouail Bellal Avatar asked Jan 15 '20 15:01

Ouail Bellal


People also ask

How do I refresh a view in SwiftUI?

To add the pull to refresh functionality to our SwiftUI List, simply use the . refreshable modifier. List(emojiSet, id: \. self) { emoji in Text(emoji) } .

What does @published mean SwiftUI?

@Published is one of the property wrappers in SwiftUI that allows us to trigger a view redraw whenever changes occur. You can use the wrapper combined with the ObservableObject protocol, but you can also use it within regular classes.

What is EnvironmentObject?

What is an @EnvironmentObject? An @EnvironmentObject is an object living in the current environment, available to read whenever needed. An environment object is defined at a higher-level view, and can any child view can access it if needed.

What is ObservableObject SwiftUI?

A type of object with a publisher that emits before the object has changed.

How to safely update the view state in SwiftUI?

Safely Updating The View State 1 Updating the State View. When SwiftUI is computing the body of a view, the state should remain unchanged. ... 2 Breaking The Loop. In the following example, we have a geometry effect that rotates an image. ... 3 Unexpected Loops. So far, things are pretty clear. ... 4 One More Thing. ... 5 In Summary. ...

What is observedobject in SwiftUI?

A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes. In this tutorial, you’ll learn what is ObservedObject in SwiftUI.

Why does SwiftUI re-evaluate my model if it's not equatable?

You learned that if the model for a view contains properties that changed, SwiftUI will re-evaluate the view's body. This is true even if the changed properties aren't used in your view. More interestingly, you saw that SwiftUI can compare your models even if they're not Equatable.

Does SwiftUI need to be re-computed every time?

As you can see, SwiftUI is wise enough to know the body does not need to be re-computed every time, only when the state really changed. That means that unless you set a different value in the state, the view will not get invalidated. In the case above: only when the cardinal direction is different it will request a new body.


2 Answers

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.

like image 119
kumar shivang Avatar answered Nov 15 '22 06:11

kumar shivang


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.

like image 21
Jim lai Avatar answered Nov 15 '22 07:11

Jim lai