Amzd / PublishedObject

A property wrapper that forwards the objectWillChange of the wrapped ObservableObject to the enclosing ObservableObject's objectWillChange.
MIT License
29 stars 8 forks source link

warning run: Publishing changes from within view updates is not allowed, this will cause undefined behavior. #7

Open degtiarev opened 1 year ago

degtiarev commented 1 year ago

Xcode Version 14.0.1 (14A400) started to show this warning

Screenshot 2022-09-30 at 16 22 57
Amzd commented 1 year ago

Is there any chance the warning is on the wrong line and you are publishing changes within view updates on your side?

Could you give a small isolated expample off when this shows? Maybe I'll have time to look into this soon.

degtiarev commented 1 year ago

Sorry for a very late reply. The warning appears pretty much all the time, even in the simplest case. For example, here:

import SwiftUI

struct ContentView: View {

    @StateObject var observableObject: SomeObservableObject
    init(text: String = "Old text") {
        _observableObject = StateObject(wrappedValue: SomeObservableObject(text: text))
    }

    var body: some View {
        VStack {
            Text(observableObject.text)
            Button {
                observableObject.text = "new text"
            } label: {
                Text("Button")
            }
        }
        .padding()
    }
}

class SomeObservableObject: ObservableObject {
    @Published var text: String

    init(text: String) {
        self.text = text
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

It seems it is Xcode 14 bug as this issue was not presented in Xcode 13.4.1, but I did not test the beta 3 or any RC version of Xcode 14.1 yet...

Amzd commented 1 year ago

In that example there isn't much point to pass the text into a state object as every time the view inits you create a new one.

degtiarev commented 1 year ago

That's just simple example when the warning appears. It did not appear before on the old Xcode version. It appears also in for example, this example:

import SwiftUI

struct ContentView: View {

    @StateObject var observableObject: SomeObservableObject = SomeObservableObject()

    var body: some View {
        VStack {
            Text(observableObject.text)
            Button {
                observableObject.text = "new text"
            } label: {
                Text("Button")
            }
        }
        .padding()
    }
}

class SomeObservableObject: ObservableObject {
    @Published var text: String

    init(text: String = "Old text") {
        self.text = text
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

And this one:

import SwiftUI

struct ContentView: View {

    @StateObject var observableObject: SomeObservableObject
    init(text: String = "Old text") {
        _observableObject = StateObject(wrappedValue: SomeObservableObject(text: text))
    }

    var body: some View {
        ZStack {
            ExtractedView(text: $observableObject.text)
        }
    }
}

struct ExtractedView: View {
    @Binding var text: String

    var body: some View {
        VStack {
            Text(text)
            Button {
                text = "new text"
            } label: {
                Text("Button")
            }
        }
        .padding()
    }
}

class SomeObservableObject: ObservableObject {
    @Published var text: String

    init(text: String) {
        self.text = text
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Which are little bit modified versions of the first one. If I set Minimum Deployments to iOS 14 and delete the library, then it does not show any warning. If you think that it is expected behaviour for these cases, please, could you give any simple example that will not have this warning.

Amzd commented 1 year ago

Hmm, you're right. I haven't looked into it but I assume it has to do with the projected value. In the init I call publisher.send() twice. The first issue I see is possible is that the queue switch there to wait for the change comes back during the view runloop. The second possible issue could be calling the publisher.send() synchronously in the init. Not sure. I'll look into it if I have time.

degtiarev commented 1 year ago

It seems putting parentObject?.objectWillChange.send() inside DispatchQueue.main.async in setParent function fixes the problem with warning.

  private func setParent<Parent: ObservableObject>(_ parentObject: Parent) where Parent.ObjectWillChangePublisher == ObservableObjectPublisher {
        guard parent.objectWillChange == nil else { return }
        parent.objectWillChange = { [weak parentObject] in
            DispatchQueue.main.async{
                parentObject?.objectWillChange.send()
            }
        }
    }