Xcode 14 “Publishing changes from within view updates is not allowed, this will cause undefined behavior”

Published on: September 7, 2022

UPDATE FOR XCODE 14.1: This issue appears to have been partially fixed in Xcode 14.1. Some occurences of the warning are fixed, others aren't. In this post I'm collecting situations me and others run into and track whether they are fixed or not. If you have another sample that you think is similar, please send a sample of your code on Twitter as a Github Gist.


Dear reader, if you've found this page you're probably encountering the error from the post title. Let me start by saying this post does not offer you a quick fix. Instead, it serves to show you the instance where I ran into this issue in Xcode 14, and why I believe this issue is a bug and not an actual issue. I've last tested this with Xcode 14.0's Release Candidate. I've filed feedback with Apple, the feedback number is FB11278036 in case you want to duplicate my issue.

Some of the SwiftUI code that I've been using fine for a long time now has recently started coming up with this purple warning.

Screenshot of "Publishing changes from within view updates is not allowed, this will cause undefined behavior." purple warning

Initially I thought that there was a chance that I was, in fact, doing something weird all along and I started chipping away at my project until I had something that was small enough to only cover a few lines, but still complex enough to represent the real world.

In this post I've collected some example of where I and other encounter this issue, along with whether it's been fixed or not.

[Fixed] Purple warnings when updating an @Published var from a Button in a List.

In my case, the issue happened with the following code:

class SampleObject: ObservableObject {
    @Published var publishedProp = 1337

    func mutate() {
        publishedProp = Int.random(in: 0...50)
    }
}

struct CellView: View {
    @ObservedObject var dataSource: SampleObject

    var body: some View {
        VStack {
            Button(action: {
                dataSource.mutate()
            }, label: {
                Text("Update property")
            })

            Text("\(dataSource.publishedProp)")
        }
    }
}

struct ContentView: View {
    @StateObject var dataSource = SampleObject()

    var body: some View {
        List {
            CellView(dataSource: dataSource)
        }
    }
}

This code really does nothing outrageous or weird. A tap on a button will simply mutate an @Published property, and I expect the list to update. Nothing fancy. However, this code still throws up the purple warning. Compiling this same project in Xcode 13.4.1 works fine, and older Xcode 14 betas also don't complain.

At this point, it seems like this might be a bug in List specifically because changing the list to a VStack or LazyVStack in a ScrollView does not give me the same warning. This tells me that there is nothing fundamentally wrong with the setup above.

Another thing that seems to work around this warning is to change the type of button that triggers the action. For example, using a bordered button as shown below also runs without the warning:

Button(action: {
    dataSource.mutate()
}, label: {
    Text("Update property")
}).buttonStyle(.bordered)

Or if you want your button to look like the default button style on iOS, you can use borderless:

Button(action: {
    dataSource.mutate()
}, label: {
    Text("Update property")
}).buttonStyle(.borderless)

It kind of looks like anything except a default Button in a List is fine.

For those reasons, I sadly cannot give you a proper fix for this issue. The things I mentioned are all workarounds IMO because the original code should work. All I can say is please file a feedback ticket with Apple so we can hopefully get this fixed, documented, or otherwise explained. I'll be requesting a code level support ticket from Apple to see if an Apple engineer can help me figure this out.

Animating a map's position in SwiftUI

A Map in SwiftUI is presented using the following code:

struct ContentView: View {
    @State var currentMapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 10.0, longitude: 0.0), span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100))

    var body: some View {
        VStack {
            Map(coordinateRegion: $currentMapRegion, annotationItems: allFriends) { friend in
                MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0)) {
                    Circle()
                        .frame(width: 20, height: 20)
                        .foregroundColor(.red)
                }
            }
        }
        .ignoresSafeArea()
    }
}

Notice how the Map takes a Binding for its coordinateRegion. This means that whenever the map changes what we're looking at, our @State can update and the other way around. We can assign a new MKCoordinateRegion to our @State property and the Map will update to show the new location. It does this without animating the change. So let's say we do want to animate to a new position. For example, by doing the following:

var body: some View {
    VStack {
        Map(coordinateRegion: $currentMapRegion, annotationItems: allFriends) { friend in
            MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: friend.cityLatitude ?? 0, longitude: friend.cityLongitude ?? 0)) {
                Circle()
                    .frame(width: 20, height: 20)
                    .foregroundColor(.red)
            }
        }
    }
    .ignoresSafeArea()
    .onAppear {
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            withAnimation {
                currentMapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 80, longitude: 80),
                                                      span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100))
            }
        }
    }
}

This code applies some delay and then eventually moves the map to a new position. The animation could also be triggered by a Button or really anything else; how we trigger the animation isn't the point.

When the animation runs, we see lots and lots of warnings in the console (187 for me...) and they all say [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior..

We're clearly just updating our currentMapRegion just once, and putting print statements in the onAppear tells us that the onAppear and the withAnimation block are all called exactly once.

I suspected that the Map itself was updating its binding to animate from one position to the next so I changed the Map setup code a little:

Map(coordinateRegion: Binding(get: {
    self.currentMapRegion
}, set: { newValue, _ in
    print("\(Date()) assigning new value \(newValue)")
    self.currentMapRegion = newValue
}), annotationItems: allFriends) { friend in
    MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: friend.cityLatitude ?? 0, longitude: friend.cityLongitude ?? 0)) {
        Circle()
            .frame(width: 20, height: 20)
            .foregroundColor(.red)
    }
}

Instead of directly binding to the currentMapRegion property, I made a custom instance of Binding that allows me to intercept any write operations to see how many occur and why. Running the code with this in place, yields an interesting result:

2022-10-26 08:38:39 +0000 assigning new value MKCoordinateRegion(center: __C.CLLocationCoordinate2D(latitude: 62.973218679210305, longitude: 79.83448028564462), span: __C.MKCoordinateSpan(latitudeDelta: 89.49072082474844, longitudeDelta: 89.0964063502501))
2022-10-26 10:38:39.169480+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.169692+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.169874+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 08:38:39 +0000 assigning new value MKCoordinateRegion(center: __C.CLLocationCoordinate2D(latitude: 63.02444217894995, longitude: 79.96021270751967), span: __C.MKCoordinateSpan(latitudeDelta: 89.39019889305074, longitudeDelta: 89.09640635025013))
2022-10-26 10:38:39.186402+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.186603+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.186785+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 08:38:39 +0000 assigning new value MKCoordinateRegion(center: __C.CLLocationCoordinate2D(latitude: 63.04063284402105, longitude: 80.00000000000011), span: __C.MKCoordinateSpan(latitudeDelta: 89.35838016069978, longitudeDelta: 89.0964063502501))
2022-10-26 10:38:39.200000+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.200369+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.200681+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.

This is just a small part of the output of course but we can clearly see that the print from the custom Binding is executed in between warnings.

I can only conclude that this has to be some issue in Map that we cannot solve ourselves. You might be able to tweak the custom binding a bunch to throttle how often it actually updates the underlying @State but I'm not sure that's what we should want...

If you're seeing this issue too, you can reference FB11720091 in feedback that you file with Apple.

Huge thanks to Tim Isenman for sending me this sample.

Categories

SwiftUI