onReceive in SwiftUI view causes infinite loop

Viewed 1564

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

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
          }
      }

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)
    }
}

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

Related