5

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)
            }
        }
    }
}
connorvo
  • 761
  • 2
  • 7
  • 21

3 Answers3

10

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.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 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) – connorvo Aug 15 '20 at 17:07
  • 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). – Rob Napier Aug 15 '20 at 17:12
  • 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? – connorvo Aug 15 '20 at 17:15
  • Yep, that's what you want to do (at least in iOS 13) – Rob Napier Aug 15 '20 at 17:21
  • 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 times – connorvo Aug 15 '20 at 17:24
  • 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 crazy – connorvo Aug 15 '20 at 17:33
  • 1
    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 NavigationLink – connorvo Aug 15 '20 at 17:36
  • 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. – Rob Napier Aug 15 '20 at 17:49
  • 1
    Thanks man you saved my day! – imike Apr 19 '22 at 23:43
  • You were sent by the swift gods! Thank you I was trying to figure this out for almost a week now. – Throvn Sep 05 '22 at 15:44
3

Try to update on main queue

API.User.searchUser(text: searchText) { (users) in
   DispatchQueue.main.async {
      self.isLoading = false
      self.users = users
   }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
1

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.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83