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.

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

Same problem. I'm not sure: is there a problem with using DispatchQueue.main.async {} to fix it? What did you do? I'm looking forward to official news (or a fix???) from Apple.

It would be a huge issue if views can't change @Published variables in the View Model.

A workaround was provided above by rayx: it's not a beautiful solution in my opinion but it works. To simplify it, I found a way to modify only the main view, so we don't need to touch any of the sheet views. Here below, I apply the solution to Cwie's code:

struct ContentView: View {

    @ObservedObject
    var viewModel: ViewModel

    // Local variable
    @State private var isPresented = false

    var body: some View {
        VStack {
            Button {
                viewModel.showSheet()
            } label: {
                Text("Show Sheet")
            }
        }
        .padding()

        // Use isPresented
        .sheet(isPresented: $isPresented) {
            if viewModel.sheetVM != nil {
                SheetView(sheetVM: sheetVM!)
            }
        }

        // The View Model controls the local variable
        .onChange(of: viewModel.sheetVM) { value in
            if value != nil {
                isPresented = true
            } else {
                isPresented = false
            }

        }        
    }
}

I hope Apple clarifies and solves this quickly.

For those who are still struggling with the issue, this is probably the simplest workaround (in both concept and implementation).

func delayedWrite<Value>(_ value: Binding<Value?>) -> Binding<Value?> {
    return Binding() {
        value.wrappedValue
    } set: { newValue in
        DispatchQueue.main.async {
            value.wrappedValue = newValue
        }
    }
}

Then just wrap the binding which causes the issue with the above function. That's it.

It's so simple that I don't mind any more if Apple would fix the issue or not. Happy hacking :)

I am having this issue merely using Map(...anotiationItems:,annotationView:...) If there are ANY annotationItems returned , then multiple times per second that error is spat out with the most useless call stack from LLDB I've seen since Xcode 5.

It has nothing to do for me, with whether map bindings are @State local to the view or in a view model.

I can see from searching online the problem isn't even acknowledged. Only option I have now is dumping apple maps I suppose. If anyone else has any ideas....

I may have a slightly different opinion about whether it's a huge thing, not to allow changes of the published properties directly from views ;) In my opinion, this could also be considered a code smell if the pattern should follow MVVM principles.

When we employ the MVVM pattern, we do not have writable published properties, only readable. Changes are notified to the view model via functions. These "intents" then get processed by the view model which in turn may change the published value, or it may not. The published value is a function of the view model, that is, it's the view model which determines the view's state, not the view.

However, even when a view model determines the published value, it still must not change it during the view updates, otherwise the same warning would occur.

There is one pattern which may alleviate the issue:

For any practical reasons, a ViewModel should be an actor. A MainActor would be suitable.

So, we start with our ViewModel to be a MainActor:

@MainActor
final class SomeViewModel: ObservableObject {
    @Published private(set) var state: State = .init()
    ... 

Notice, that the published value state needs to have an initial value, and it's setter is private.

In order to let a view express an intent, which may change the state, we define a function:

@MainActor
final class SomeViewModel: ObservableObject {
    @Published private(set) var state: State = .init()
    

    nonisolated func updateValue(_ value: T) {
        ...
    } 

Note, the function updateValue(_:) is declared nonisolated.

In order to make it work with the main actor though, we need to dispatch onto the main thread. This will be accomplished with another private helper function, which will be executed on the actor:

    nonisolated func updateValue(_ value: T) {
        Task {
            await _updateValue(value)
        }
    } 

   private func _updateValue(_ value: T) {
       let newValue = someLogic(value)
       self.state.value = newValue
   }

This has two desired side effects:

  1. First we dispatch on the main thread (_updateValue(_:) which is required by the actor
  2. And because of the dispatching we ensure (or strongly assume) that the view update phase is finished when this will get executed (thus, no warnings anymore)

Note: the function someLogic(_: T) should be a pure, synchronous function, which determines the logic, i.e. the new state, based on the current state and the input.

Now in a View we can call updateValue(:_) from anywhere, for example:

    Button {
        viewModel.updateValue(false)
    }

Note: the action function is NOT declared to be executed on the main thread, even though it will be executed on the main thread. An isolated main actor function would require the closure to be dispatched explicitly onto the main thread. However, this function is nonisolated - thus, we can call it from any thread.

Of course, we also can construct a binding in the body of a parent view and pass this along to child views:

    let binding: Binding<Bool > = .init(
        get { viewModel.state.value }
        set { value in viewModel.updateValue(value) }
    )

So, with this "pattern" we get some desired benefits. We also "fix" this issue due to the dispatching.

This problem is still occurring in Xcode Version 14.0.1 (14A400).

The context is a button that's pressed to exit a view that is controlled with:

  .fullScreenCover(isPresented: $settings.showWorkout)

Wrapping it in DispatchQueue.main.asyncAfter made no difference.

I honestly have no idea what's wrong with this or how it can be done differently.

It doesn't seem to cause any harm. Will an app be rejected with these kinds of warnings?

Mine wasn't rejected and I've made several updates already also.

I get this warning for certain view hierarchies. The following code triggers the warning, but if I move the Button out of the Form or change Form to be a VStack, the warning disappears. It seems like a bug to me, and I have filed a bug report.

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        Form {
            Button("Save") {
                viewModel.counter += 1
            }
        }
    }
}

class ViewModel: ObservableObject {
    @Published var counter = 0
}

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.

Xcode 14: Publishing changes from within view updates
 
 
Q