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

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'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.

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.

23

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 .

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.

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

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/

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.

Xcode 14: Publishing changes from within view updates
 
 
Q