SwiftUI: calling function in View body that modifies EnvironmentObject results in infinite loop and app crash

Viewed 963

I'm designing a macOS app with SwiftUI. I need to modify an EnvironmentObject whose value is calculated from a State variable. To do this, I tried to call a method that does the calculation and updates the EnvironmentObject, but the method appears to fall into an infinite loop that causes the app to stop responding.

Can someone explain what's going on? And, how do I achieve the same ends without having this problem?

I wrote this code to demonstrate what I mean:

final class Input: ObservableObject {
    @Published var bar = 0
}

struct ContentView: View {

    @State var foo = 0
    @EnvironmentObject var input: Input

    func increment(code: Int) {

        print("function called")

        switch code {
        case 0:
            input.bar -= 1
        case 1:
            input.bar += 1
        default:
            print("unknown")
        }

        print("\(input.bar)")

    }

    var body: some View {
        increment(code: foo)
        return Text("Hello world!")     // In reality this is code that modifies foo
    }

}

When this code is run, the console prints "function called" in an infinite loop, and also prints continuously decreasing values of input.bar. From my experimentation, I found that:

  • Replacing input.bar with some other code (e.g. a simple print command) does not cause an infinite loop.

  • With this same code, setting foo to 2 (thus giving the default case) also doesn't cause an infinite loop.

  • foo doesn't need to be a State var, the app crashes even if it's a simple variable.

1 Answers

The simplest solution is to call observable object calculating method in the place where internal state changed, as in following example.

final class Input: ObservableObject {
    @Published var bar = 0


    func increment(code: Int) { // it belongs to Input, so should be here

        print("function called")

        switch code {
        case 0:
            bar -= 1
        case 1:
            bar += 1
        default:
            print("unknown")
        }

        print("\(bar)")

    }
}

struct ContentView: View {

    @State var foo = 0
    @EnvironmentObject var input: Input

    var body: some View {
        Button("Do Something") {
            self.foo = 1          // << changed state
            self.input.increment(code: 1) // << updated calculable
        }
    }

}
Related