6

In a SwiftUI app, I have an ObservableObject that keeps track of user settings:

class UserSettings: ObservableObject {
    @Published var setting: String?
}

I have a view model to control the state for my view:

class TestViewModel: ObservableObject {
    @Published var state: String = ""
}

And I have my view. When the user setting changes, I want to get the view model to update the state of the view:

struct HomeView: View {
    @EnvironmentObject var userSettings: UserSettings
    @ObservedObject var viewModel = TestViewModel()

    var body: some View {
        Text(viewModel.state)
            .onReceive(userSettings.$setting) { setting in
                self.viewModel.state = setting
            }
    }
}

When the UserSettings.setting is changed in another view it causes onReceive on my view to get called in an infinite loop, and I don't understand why. I saw this question, and that loop makes sense to me because the state of the ObservableObject being observed is being changed on observation.

However, in my case I'm not changing the observed object (environment object) state. I'm observing the environment object and changing the view model state which redraws the view.

Is the view redrawing what's causing the issue here? Does onReceive get called everytime the view is redrawn?

Is there a better way of accomplishing what I'm trying to do?

EDIT: this is a greatly simplified version of my problem. In my app, the view model takes care of executing a network request based on the user's settings and updating the view's state such as displaying an error message or loading indicator.

3 Answers 3

5

Whenever you have an onReceive with an @ObservedObject that sets another (or the same) published value of the @ObservedObject you risk creating an infinite loop if those published attributes are being displayed somehow.

Make your onReceive verify that the received value is actually updating a value, and not merely setting the same value, otherwise it will be setting/redrawing infinitely. In this case, e.g.,:

.onReceive(userSettings.$setting) { setting in
          if setting != self.viewModel.state {
            self.viewModel.state = setting
          }
      }
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you!!! This was literally the only thing that was keeping my code to work. You are a legend!!
You can still get loops if you eg. manage to send two updates at once!
2

From described scenario I don't see the reason to duplicate setting in view model. You can show the value directly from userSettings, as in

struct HomeView: View {
    @EnvironmentObject var userSettings: UserSettings
    @ObservedObject var viewModel = TestViewModel()

    var body: some View {
        Text(userSettings.setting)
    }
}

3 Comments

thanks for taking the time to answer, but as I explained in an edit to my question, I do need to set the viewModel. In my app, I need to call a function in the view model to execute a network request and the view model sets the state of the view as the request executes (such as displaying error message, loading indicator, etc.)
Were you able to think of why the looping behavior is happening? That is the real head-scratcher for me.
@gnarlybracket, on simplified question the simplified answer, as I don't see real code you need to extrapolate it yourself - the cycling in logic, not in swiftui - the solution, as shown, to break it.
1

You might be able to prevent infinite re-rendering of the view body by switching your @ObservedObject to @StateObject.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.