SwiftUI ObservableObject not updating when value is Published
Asked Answered
E

3

6

I have a view and a viewModel that should update the ListView when users are added to the user array. I can verify that users are being added, yet the ObservedObject is not updating.

I have a search bar that lets you search users and then updates user array in the ViewModel which is supposed to update the View but it doesn't.

ViewModel

class UsersViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false
    var searchText: String = ""
    
    func searchTextDidChange() {
        isLoading = true
        API.User.searchUser(text: searchText) { (users) in
            self.isLoading = false
            self.users = users
        }
        // confirmed that users has data now at this point
    }
}

View

struct UsersView: View {
    @ObservedObject var usersViewModel = UsersViewModel()
    
    var body: some View {
        VStack() {
            SearchBarView(text: $usersViewModel.searchText, onSearchButtonChanged: usersViewModel.searchTextDidChange)
            
            // Not getting called after usersViewModel.users gets data
            if (usersViewModel.users.count > 0) {
                Text(usersViewModel.users[0].username)
            }
        }
    }
}
Ens answered 15/8, 2020 at 15:19 Comment(0)
S
11

You are likely winding up with different UsersViewModel objects:

@ObservedObject var usersViewModel = UsersViewModel()

Since UsersView is a struct, this creates a new model every time the View is instantiated (which can happen very often, not just when the view appears). In iOS 14 there is @StateObject to combine State (which preserves information between View instantiations) with ObservedObject, but in iOS 13 I recommend passing in the ObservedObject if it's not a shared instance.

Slype answered 15/8, 2020 at 16:54 Comment(10)
I don't think this is it. It seems to be a problem with the async data. If I create a button in the view and then just have it toggle a value in the ViewModel, that works fine. But if I run it through my asynchronous API call, it doesn't work (even though the async call returns data and updates the Published variable)Ens
If my theory is correct, you can check by putting a breakpoint in UsersViewModel.init. If it fires more than once, then you're having this problem (even if it isn't the cause of this specific bug).Slype
You are indeed correct. It's getting initialized a bunch. SO you are saying I should be creating the ViewModel prior to instantiating the View and then just pass the ViewModel in?Ens
Yep, that's what you want to do (at least in iOS 13)Slype
I'm entering the view through a NavigationLink in a NavigationBarButton. The NavigationBarButton gets called 3 times whenever I tap it so even if I pass in the UserViewModel() to the View it is still getting initialized multiple times because the View is getting initialized multiple times because the NavigationLin is getting initialized multiple times because the NavigationBarButton gets initialized multiple timesEns
I can make the NavigationLink a LazyView or I guess I have to instantiate my ViewModel in the view that has the NavigationLink and pass it to the navigationLink which is instantiating my user view. That seems crazyEns
Appreciate your help. I've gotten it working by instantiating the ViewModel in View1 and then passing the ViewModel to my NavigationLink in View1 which instantiates View2. Just seems wild that I have to instantiate the ViewModel in a totally separate view that doesn't use it and may not ever even open View2 unless someone clicks on the NavigationLinkEns
In iOS13, this is often addressed with .onAppear which delays initialization until the view is on screen (though there are many bugs with that, particularly if you use TabViews I find that it can be called many more times than expected). But iOS 14 adds @StateObject specifically to address this.Slype
Thanks man you saved my day!Randi
You were sent by the swift gods! Thank you I was trying to figure this out for almost a week now.Samellasameness
E
3

Try to update on main queue

API.User.searchUser(text: searchText) { (users) in
   DispatchQueue.main.async {
      self.isLoading = false
      self.users = users
   }
}
Etiquette answered 15/8, 2020 at 15:27 Comment(1)
Nope. Nothing stillEns
M
2

If your view is inside another view and you are not injecting the view model, consider using @StateObject.

This will not cause the object to be renewed every time the view is re-rendered.

Manatarms answered 22/12, 2022 at 6:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.