Xcode 14: Publishing changes from within view updates

After updating to Xcode 14.0 beta 5 (14A5294e) I'm seeing runtime warnings on iOS when simply changing a published variable from within a button's action.

I'm wondering if this is a false positive on the warning or if there is a new pattern we're supposed to be following here.

This appears to only happen on iOS. The same reproducing code does not output that warning while running on macOS.

Here's a contrived yet complete example that reproduces it on an iPhone 13 Pro simulator while running Xcode 14 beta 5. Does not reproduce the warning when running on Ventura.



class ViewModel: ObservableObject {
    @Published var sheetVM: SheetVM?

    func showSheet() {
        sheetVM = SheetVM(delegate: self)
    }

}

extension ViewModel: SheetDelegate {
    func close() {
        sheetVM = nil
    }

}

class SheetVM: ObservableObject, Identifiable {

    weak var delegate: SheetDelegate?

    init(delegate: SheetDelegate) {
        self.delegate = delegate
    }

    func close() {
        delegate?.close()
    }
}

protocol SheetDelegate: AnyObject {
    func close()
}

struct ContentView: View {

    @ObservedObject
    var viewModel: ViewModel

    var body: some View {
        VStack {
            Button {
                viewModel.showSheet()
            } label: {
                Text("Show Sheet")
            }
        }
        .padding()
        .sheet(item: $viewModel.sheetVM) { sheetVM in
            SheetView(sheetVM: sheetVM)
        }
    }
}

struct SheetView: View {

    let sheetVM: SheetVM

    var body: some View {
        NavigationView {
            Text("Sheet View")
                .toolbar {
                    ToolbarItem(placement: .automatic) {
                        Button {
                            sheetVM.close()
                        } label: {
                            Text("Done")
                                .fontWeight(.semibold)

                        }

                    }

                }
        }

    }

}

Here's the warning and trace while tapping done on the sheet:

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

Thread 1
#0	0x0000000000000000 in ___lldb_unnamed_symbol155969 ()
#1	0x0000000000000000 in ___lldb_unnamed_symbol155968 ()
#2	0x0000000000000000 in ___lldb_unnamed_symbol155988 ()
#3	0x0000000000000000 in ___lldb_unnamed_symbol180158 ()
#4	0x0000000000000000 in ObservableObjectPublisher.Inner.send() ()
#5	0x0000000000000000 in ObservableObjectPublisher.send() ()
#6	0x0000000000000000 in PublishedSubject.send(_:) ()
#7	0x0000000000000000 in specialized static Published.subscript.setter ()
#8	0x0000000000000000 in static Published.subscript.setter ()
#9	0x0000000000000000 in ViewModel.sheetVM.setter ()
#10	0x0000000000000000 in key path setter for ViewModel.sheetVM : ViewModel ()
#11	0x0000000000000000 in NonmutatingWritebackBuffer.__deallocating_deinit ()
#12	0x0000000000000000 in _swift_release_dealloc ()

Post not yet marked as solved Up vote post of Cwie Down vote post of Cwie
15k views
  • I'm seeing the same warning, as well as significantly decreased performance in my SwiftUI app on iOS 16 (none of these issues on iOS 15). Unsure if these are due to to an Xcode/iOS bug or if I'm actually doing something wrong.

  • The problem comes from using a Binding variable as a ObservableObject, in ios 16 it gives you this warning not to use Bindings with Observables. For a good explanation of how and why, here's a link to " Rebeloper - Rebel Developer" video on YouTube.

    https://youtu.be/3a7tuhVpoTQ

    Hope this will help.

Add a Comment

Replies

A very basic example - same behavior... '[SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.'

import SwiftUI

//MARK: Content View
struct ContentView: View {
    @StateObject
    private var someVM: SomeViewModel = .shared
    var body: some View {
        VStack {
            if let value = someVM.bindingValue {
                HStack {
                    Text("value:".capitalized)
                    Text(value.description)
                }
                .font(.largeTitle)
            }
            Button {
                someVM.isPresented.toggle()
            } label: {
                Image(systemName: "exclamationmark.triangle.fill")
                    .foregroundColor(.orange)
                Text("Hello, Beta 5 !")
            }
            .font(.title2)
            .buttonStyle(.bordered)
        }
        .padding()
        .sheet(isPresented: $someVM.isPresented) {
            SheetView(someVM: .shared)
        }
    }
}


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


//MARK: Sheet View
struct SheetView: View {
    @ObservedObject
    var someVM: SomeViewModel
    var body: some View {
        NavigationStack {
            List {
                ForEach(0..<10) { value in
                    Button {
                        someVM.updateValue(for: value)
                    } label: {
                        HStack {
                            Text("value".capitalized)
                            Spacer()
                            Text(value.description)
                                .bold()
                                .foregroundColor(.red)
                        }
                    }
                }
            }
            .navigationTitle("Sheet View")
        }
    }
}


//MARK: View Model
final class SomeViewModel: ObservableObject {
    static let shared: SomeViewModel = .init()
    @Published
    private (set) var bindingValue: Int? = nil
    @Published
    var isPresented: Bool = false
    func updateValue(for value: Int) {
        Task { @MainActor in
            bindingValue = value
            isPresented.toggle()
        }
    }
}

I ran into the same issue after upgrading to Xcode 14.0 beta 5. After spending one day in clueless experiments, I finally figured out what the root cause is and how to fix it. I think the error message isn't clear. At first sight, it seems to suggest that it's not allowed to change another published property (for example, in sink()) when one published property changes. That's not true (otherwise it would be a big limitation and defeat the purpose of the data model). From my experiments what it means is a scenario like the following:

  • Views like Sheet(), Picker(), etc. take a binding and change the binding implicitly.
  • You pass a view model's published property to those views as the binding parameter.
  • In your view model's code, when that published property changes, other published properties are changed accordingly.

This is the scenario not allowed in beta 5.

So, how to solve the issue? My solution is to introduce a local state to avoid simultaneous changes and use onChange() to sync the local state change with view model. This requires a little bit more code but the error message is gone.

Below is a simple example to demonstrate the issue:

import SwiftUI

final class ViewModel: ObservableObject {
    static let shared: ViewModel = .init()

    @Published var isPresented: Bool = false
}

struct ContentView: View {
    @StateObject var vm: ViewModel = .shared

    var body: some View {
        VStack(spacing: 8) {
            Button("Show Sheet") {
                vm.isPresented.toggle()
            }
        }
        .sheet(isPresented: $vm.isPresented) {
            SheetView(vm: .shared)
        }
    }
}

struct SheetView: View {
    @ObservedObject var vm: ViewModel

    var body: some View {
        NavigationStack {
            Button("Change Values in VM") {
                // Calling dismiss() have the same issue
                vm.isPresented = false
            }
            .navigationTitle("Sheet View")
        }
    }
}

Below is an example demonstrating how to fix the above issue:

import SwiftUI

final class ViewModel: ObservableObject {
    static let shared: ViewModel = .init()

    @Published var isPresented: Bool = false
}

struct ContentView: View {
    @StateObject var vm: ViewModel = .shared
    @State var isPresented: Bool = false

    var body: some View {
        VStack(spacing: 8) {
            Button("Show Sheet") {
                isPresented.toggle()
            }
        }
        .sheet(isPresented: $isPresented) {
            SheetView(vm: .shared)
        }
        .onChange(of: isPresented) { isPresented in
            vm.isPresented = isPresented
        }
    }
}

struct SheetView: View {
    @Environment(\.dismiss) var dismiss
    @ObservedObject var vm: ViewModel

    var body: some View {
        NavigationStack {
            Button("Change Values in VM") {
                dismiss()
            }
            .navigationTitle("Sheet View")
        }
    }
}

I think this is quite a big change in SwiftUI and I can't believe it shows up as late as in beta 5. My guess is that it has been in SwiftUI 4 since the first day but the error message was added in beta 5. Again I really don't understand why SwiftUI team can't publish an architecture paper for each release to cover important designs like this but just keep the developers outside Apple trying and guessing.

Post not yet marked as solved Up vote reply of rayx Down vote reply of rayx
  • Whoa. Thanks rayx. This is huge and completely undermines the VM approach. I echo your reaction.

  • One more thing. When I wrote the above I assumed the error occurred for all Views that wrote to its binding param. My further experiments show that's not true. For example, TextField doesn't have this issue. So it seems only some Views (e.g. Picker, Sheet) have this issue. We need an official answer from the SwiftUI team on what exactly the error message means. Before that I'll continue to use my solution above.

  • Same problem in Picker between View's. Thank you @rayx

Same issue here with a .sheet that has its "isPresented" state in an ObservedObject :/

That's my scenario also (sheet).

The same thing happens to me as well, but only in the last xcode beta version (v14 beta 5). I have a very veeery simple use case:

NavigationStack(path: $router.homeNavPath) {
                Form {
                    Section {
                        ForEach(Item.dummyItems) { item in
                            NavigationLink(value: item) {
                                LabeledContent(item.name, value: item.price, format: .number)
                            }
                        }
                    }
                }
             .navigationDestination(for: Route.self) { route in
                 switch route {
                 case .testHomeDetailView(let item):
                    DummyViewHere()
                 }
            }
}

final class Router: ObservableObject {
    @Published var homeNavPath = NavigationPath() 
}

The above view is literally everything i have inside the HomeView's body, nothing more.

Inside the main app struct I have this:

@main
struct Test_App: App {
    @StateObject private var router = Router()

    var body: some Scene {
        WindowGroup {
            HomeView()
                .environmentObject(router)
        }
    }
}

If I tap on any item from that list it navigates to the dummy detail view but after not a second it pops back the screen to the home view.... this didn't happen in the previous versions of xcode...

Furthermore if I add a second section to the form like so:

                   Section {
                        VStack {
                            Text("Home view")
                            Button("navigate to test detail screen") {
                                router.homeNavPath.append(Item.dummyItems[0])
                            }
                        }
                    }

Now if I tap the second section's button it behaves like before (it's going to the detail screen and gets almost instantly popped back), but as an extra I also get this warning:

Publishing changes from within view updates is not allowed, this will cause undefined behavior

It doesn't matter if you replaced Form and Section with just a plain ScrollView or not, or if you replace the ForEach with a List, the behaviour is gonna be the same.

Furthermore if I use NavigationSplitView and i have a NavigationStack in the detail callback with the same path of router.path i get these warnings now:

Test[56762:693175] [UILog] Called -[UIContextMenuInteraction updateVisibleMenuWithBlock:] while no context menu is visible. This won't do anything.

Update NavigationRequestObserver tried to update multiple times per frame.

Update NavigationAuthority bound path tried to update multiple times per frame.

Now for the "Update NavigationRequestObserver" warning I haven't found anything yet anywhere which is kinda scary in a way.

I'm pretty sure it's some sort of a bug in this beta version, but if it's not, then the new navigation api has got some serious issues. I really hope it's gonna be ok and fixed asap.

EDIT:

I found out in the end that the reason why it's navigating to the detail screen, but it's popping back in not even a second after, is because inside the DummyViewHere() (the detail view basically) you must NOT have a NavigationStack, but just build the content that would go in the nav view. I added the navigation stack by mistake in the detail view, even though it was just one place where i did this mistake. Lack of attention... Still the other issues persist.

Not fixed on Xcode Version 14.0 beta 6 (14A5294g) 👎

wizard_dev is a genius. The (temporary) solution for the problem, that for a view that appears in the .sheet is to kill NavigationView/Stack that goes into this view.

With Form .buttonStyle(BorderlessButtonStyle()) on the Stack or Button fixed the Issue .

  • OK, this is really strange. How it makes a difference for the runtime?

  • I think the Error comes from a kinda hit-test Layer problem with the Default Button in some Cases of Navigation Stack or Form. In my Form, without the modifier, all Buttons in a Stack are hit together. The modifier fixed it just by accident. On runtime, you just don't get the correct root of the Error. It's a strange Bug indeed

  • It worked! Let's hope this is a legit fix. Thank you @postng!

Add a Comment

here we go: https://www.icloud.com/iclouddrive/04dSMJxaiAECnnpJ0QVQzITug#formBug

Is there any way to do list pagination with this? I've got a Set where rows have an onAppear() that at a certain point trigger additional data to load and this message comes up repeatedly. I haven't been able to figure out a way around it.

Add a Comment

Unfortunately, I have the same issue in Version 14.0 (14A309) (release version).

I've thought out another workaround. It may not be acceptable for some scenarios but in other it may be bearable. Below is the example of 2 ObservableObject classes --> Object1 produces the error, Object2 does not.

class Object1: ObservableObject {

    @Published var title = ""

    func setTitle(to newTitle: String) {
        title = newTitle // this causes the error in question ("Publishing changes from within view updates is not allowed, this will cause undefined behavior.")
    }
}

class Object2: ObservableObject {

    @Published var title = ""
    
    func setTitle(to newTitle: String) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // performing the change later on removes the error because obviously it is not "within view updates"
            self.title = newTitle
        }
    }
}
  • I don't think the actual delay is necessary. If you just use DispatchQueue.main.async {} it seems to solve it as well. I'm just not sure what the side effects of this would be?

  • Thank you, that fixes the issue for me. Indeed DispatchQueue.main.async {} is enough.

Add a Comment

Found an interesting blog post about this.

https://www.donnywals.com/xcode-14-publishing-changes-from-within-view-updates-is-not-allowed-this-will-cause-undefined-behavior/

  • Even this blog did not know what to do or how to properly fix this issue.

  • That link says it's fixed in 14.1b3, but we're still seeing the problem in our app.

Add a Comment

This is getting my application to crash and it is not okay to implement the solution proposed by some people here because it defies everything good in the MVVM architecture, is this going to be fixed from apple side soon or are we going to have to use the workaround ?

Ok I think this might be bug. Since Xcode 14 beta 1 (https://xcodereleases.com) I don't see this warning anymore.

same problem! when i use fullscreencover present from a to b and use environment publish var set it to nil to close b will show this warn many times what should i do? thanks